mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
feat(ollama): Add Ollama provider support with models and configuration, including API endpoints and UI updates.
This commit is contained in:
@@ -424,6 +424,10 @@ export const PROVIDERS = {
|
|||||||
baseUrl: "https://api.hyperbolic.xyz/v1/chat/completions",
|
baseUrl: "https://api.hyperbolic.xyz/v1/chat/completions",
|
||||||
format: "openai"
|
format: "openai"
|
||||||
},
|
},
|
||||||
|
ollama: {
|
||||||
|
baseUrl: "https://ollama.com/api/chat",
|
||||||
|
format: "openai"
|
||||||
|
},
|
||||||
deepgram: {
|
deepgram: {
|
||||||
baseUrl: "https://api.deepgram.com/v1/listen",
|
baseUrl: "https://api.deepgram.com/v1/listen",
|
||||||
format: "openai"
|
format: "openai"
|
||||||
|
|||||||
@@ -301,6 +301,9 @@ export const PROVIDER_MODELS = {
|
|||||||
{ id: "Qwen/Qwen2.5-Coder-32B-Instruct", name: "Qwen 2.5 Coder 32B" },
|
{ id: "Qwen/Qwen2.5-Coder-32B-Instruct", name: "Qwen 2.5 Coder 32B" },
|
||||||
{ id: "NousResearch/Hermes-3-Llama-3.1-70B", name: "Hermes 3 70B" },
|
{ id: "NousResearch/Hermes-3-Llama-3.1-70B", name: "Hermes 3 70B" },
|
||||||
],
|
],
|
||||||
|
ollama: [
|
||||||
|
{ id: "gpt-oss:120b", name: "GPT OSS 120B" },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
@@ -372,6 +375,7 @@ export const PROVIDER_ID_TO_ALIAS = {
|
|||||||
nebius: "nebius",
|
nebius: "nebius",
|
||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
hyperbolic: "hyperbolic",
|
hyperbolic: "hyperbolic",
|
||||||
|
ollama: "ollama",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getModelsByProviderId(providerId) {
|
export function getModelsByProviderId(providerId) {
|
||||||
|
|||||||
BIN
public/providers/ollama.png
Normal file
BIN
public/providers/ollama.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -214,11 +214,10 @@ export default function ProvidersPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleBatchTest("oauth")}
|
onClick={() => handleBatchTest("oauth")}
|
||||||
disabled={!!testingMode}
|
disabled={!!testingMode}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${testingMode === "oauth"
|
||||||
testingMode === "oauth"
|
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
}`}
|
||||||
}`}
|
|
||||||
title="Test all OAuth connections"
|
title="Test all OAuth connections"
|
||||||
aria-label="Test all OAuth connections"
|
aria-label="Test all OAuth connections"
|
||||||
>
|
>
|
||||||
@@ -252,12 +251,11 @@ export default function ProvidersPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleBatchTest("free")}
|
onClick={() => handleBatchTest("free")}
|
||||||
disabled={!!testingMode}
|
disabled={!!testingMode}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${testingMode === "free"
|
||||||
testingMode === "free"
|
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
|
||||||
}`}
|
}`}
|
||||||
title="Test all Free connections"
|
title="Test all Free connections"
|
||||||
aria-label="Test all Free provider connections"
|
aria-label="Test all Free provider connections"
|
||||||
>
|
>
|
||||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "free" ? " animate-spin" : ""}`}>
|
<span className={`material-symbols-outlined text-[14px]${testingMode === "free" ? " animate-spin" : ""}`}>
|
||||||
@@ -289,12 +287,11 @@ export default function ProvidersPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleBatchTest("apikey")}
|
onClick={() => handleBatchTest("apikey")}
|
||||||
disabled={!!testingMode}
|
disabled={!!testingMode}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${testingMode === "apikey"
|
||||||
testingMode === "apikey"
|
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
|
||||||
}`}
|
}`}
|
||||||
title="Test all API Key connections"
|
title="Test all API Key connections"
|
||||||
aria-label="Test all API Key connections"
|
aria-label="Test all API Key connections"
|
||||||
>
|
>
|
||||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "apikey" ? " animate-spin" : ""}`}>
|
<span className={`material-symbols-outlined text-[14px]${testingMode === "apikey" ? " animate-spin" : ""}`}>
|
||||||
@@ -328,11 +325,10 @@ export default function ProvidersPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleBatchTest("compatible")}
|
onClick={() => handleBatchTest("compatible")}
|
||||||
disabled={!!testingMode}
|
disabled={!!testingMode}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${testingMode === "compatible"
|
||||||
testingMode === "compatible"
|
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
}`}
|
||||||
}`}
|
|
||||||
title="Test all Compatible connections"
|
title="Test all Compatible connections"
|
||||||
>
|
>
|
||||||
<span className={`material-symbols-outlined text-[14px]${testingMode === "compatible" ? " animate-spin" : ""}`}>
|
<span className={`material-symbols-outlined text-[14px]${testingMode === "compatible" ? " animate-spin" : ""}`}>
|
||||||
@@ -449,7 +445,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="size-8 rounded-lg flex items-center justify-center"
|
className="size-8 rounded-lg flex items-center justify-center"
|
||||||
style={{ backgroundColor: `${provider.color}15` }}
|
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}` }}
|
||||||
>
|
>
|
||||||
{imgError ? (
|
{imgError ? (
|
||||||
<span className="text-xs font-bold" style={{ color: provider.color }}>
|
<span className="text-xs font-bold" style={{ color: provider.color }}>
|
||||||
@@ -501,7 +497,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
|
|||||||
<Toggle
|
<Toggle
|
||||||
size="sm"
|
size="sm"
|
||||||
checked={!allDisabled}
|
checked={!allDisabled}
|
||||||
onChange={() => {}}
|
onChange={() => { }}
|
||||||
title={allDisabled ? "Enable provider" : "Disable provider"}
|
title={allDisabled ? "Enable provider" : "Disable provider"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -561,7 +557,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="size-8 rounded-lg flex items-center justify-center"
|
className="size-8 rounded-lg flex items-center justify-center"
|
||||||
style={{ backgroundColor: `${provider.color}15` }}
|
style={{ backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}` }}
|
||||||
>
|
>
|
||||||
{imgError ? (
|
{imgError ? (
|
||||||
<span className="text-xs font-bold" style={{ color: provider.color }}>
|
<span className="text-xs font-bold" style={{ color: provider.color }}>
|
||||||
@@ -621,7 +617,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
|
|||||||
<Toggle
|
<Toggle
|
||||||
size="sm"
|
size="sm"
|
||||||
checked={!allDisabled}
|
checked={!allDisabled}
|
||||||
onChange={() => {}}
|
onChange={() => { }}
|
||||||
title={allDisabled ? "Enable provider" : "Disable provider"}
|
title={allDisabled ? "Enable provider" : "Disable provider"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -955,9 +951,8 @@ function ProviderTestResultsView({ results }) {
|
|||||||
<span className="text-text-muted font-mono tabular-nums">{r.latencyMs}ms</span>
|
<span className="text-text-muted font-mono tabular-nums">{r.latencyMs}ms</span>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
|
className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${r.valid ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400"
|
||||||
r.valid ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{r.valid ? "OK" : r.diagnosis?.type || "ERROR"}
|
{r.valid ? "OK" : r.diagnosis?.type || "ERROR"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ const PROVIDER_MODELS_CONFIG = {
|
|||||||
nebius: createOpenAIModelsConfig("https://api.studio.nebius.ai/v1/models"),
|
nebius: createOpenAIModelsConfig("https://api.studio.nebius.ai/v1/models"),
|
||||||
siliconflow: createOpenAIModelsConfig("https://api.siliconflow.cn/v1/models"),
|
siliconflow: createOpenAIModelsConfig("https://api.siliconflow.cn/v1/models"),
|
||||||
hyperbolic: createOpenAIModelsConfig("https://api.hyperbolic.xyz/v1/models"),
|
hyperbolic: createOpenAIModelsConfig("https://api.hyperbolic.xyz/v1/models"),
|
||||||
|
ollama: createOpenAIModelsConfig("https://ollama.com/api/tags"),
|
||||||
nanobanana: createOpenAIModelsConfig("https://api.nanobananaapi.ai/v1/models"),
|
nanobanana: createOpenAIModelsConfig("https://api.nanobananaapi.ai/v1/models"),
|
||||||
chutes: createOpenAIModelsConfig("https://llm.chutes.ai/v1/models"),
|
chutes: createOpenAIModelsConfig("https://llm.chutes.ai/v1/models"),
|
||||||
nvidia: createOpenAIModelsConfig("https://integrate.api.nvidia.com/v1/models"),
|
nvidia: createOpenAIModelsConfig("https://integrate.api.nvidia.com/v1/models"),
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
|
|||||||
const res = await fetchWithConnectionProxy("https://api.hyperbolic.xyz/v1/models", { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy);
|
const res = await fetchWithConnectionProxy("https://api.hyperbolic.xyz/v1/models", { headers: { Authorization: `Bearer ${connection.apiKey}` } }, effectiveProxy);
|
||||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||||
}
|
}
|
||||||
|
case "ollama": {
|
||||||
|
const res = await fetch("https://ollama.com/api/tags", { headers: { Authorization: `Bearer ${connection.apiKey}` } });
|
||||||
|
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||||
|
}
|
||||||
case "deepgram": {
|
case "deepgram": {
|
||||||
const res = await fetchWithConnectionProxy("https://api.deepgram.com/v1/projects", { headers: { Authorization: `Token ${connection.apiKey}` } }, effectiveProxy);
|
const res = await fetchWithConnectionProxy("https://api.deepgram.com/v1/projects", { headers: { Authorization: `Token ${connection.apiKey}` } }, effectiveProxy);
|
||||||
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export async function POST(request) {
|
|||||||
case "nebius":
|
case "nebius":
|
||||||
case "siliconflow":
|
case "siliconflow":
|
||||||
case "hyperbolic":
|
case "hyperbolic":
|
||||||
|
case "ollama":
|
||||||
case "assemblyai":
|
case "assemblyai":
|
||||||
case "nanobanana":
|
case "nanobanana":
|
||||||
case "chutes":
|
case "chutes":
|
||||||
@@ -180,6 +181,7 @@ export async function POST(request) {
|
|||||||
nebius: "https://api.studio.nebius.ai/v1/models",
|
nebius: "https://api.studio.nebius.ai/v1/models",
|
||||||
siliconflow: "https://api.siliconflow.cn/v1/models",
|
siliconflow: "https://api.siliconflow.cn/v1/models",
|
||||||
hyperbolic: "https://api.hyperbolic.xyz/v1/models",
|
hyperbolic: "https://api.hyperbolic.xyz/v1/models",
|
||||||
|
ollama: "https://ollama.com/api/tags",
|
||||||
assemblyai: "https://api.assemblyai.com/v1/account",
|
assemblyai: "https://api.assemblyai.com/v1/account",
|
||||||
nanobanana: "https://api.nanobananaapi.ai/v1/models",
|
nanobanana: "https://api.nanobananaapi.ai/v1/models",
|
||||||
chutes: "https://llm.chutes.ai/v1/models",
|
chutes: "https://llm.chutes.ai/v1/models",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const PROVIDER_ENDPOINTS = {
|
|||||||
openai: "https://api.openai.com/v1/chat/completions",
|
openai: "https://api.openai.com/v1/chat/completions",
|
||||||
anthropic: "https://api.anthropic.com/v1/messages",
|
anthropic: "https://api.anthropic.com/v1/messages",
|
||||||
gemini: "https://generativelanguage.googleapis.com/v1beta/models",
|
gemini: "https://generativelanguage.googleapis.com/v1beta/models",
|
||||||
|
ollama: "https://ollama.com/api/chat",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export from providers.js for backward compatibility
|
// Re-export from providers.js for backward compatibility
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ export const APIKEY_PROVIDERS = {
|
|||||||
hyperbolic: { id: "hyperbolic", alias: "hyp", name: "Hyperbolic", icon: "bolt", color: "#00D4FF", textIcon: "HY", website: "https://hyperbolic.xyz" },
|
hyperbolic: { id: "hyperbolic", alias: "hyp", name: "Hyperbolic", icon: "bolt", color: "#00D4FF", textIcon: "HY", website: "https://hyperbolic.xyz" },
|
||||||
deepgram: { id: "deepgram", alias: "dg", name: "Deepgram", icon: "mic", color: "#13EF93", textIcon: "DG", website: "https://deepgram.com" },
|
deepgram: { id: "deepgram", alias: "dg", name: "Deepgram", icon: "mic", color: "#13EF93", textIcon: "DG", website: "https://deepgram.com" },
|
||||||
assemblyai: { id: "assemblyai", alias: "aai", name: "AssemblyAI", icon: "record_voice_over", color: "#0062FF", textIcon: "AA", website: "https://assemblyai.com" },
|
assemblyai: { id: "assemblyai", alias: "aai", name: "AssemblyAI", icon: "record_voice_over", color: "#0062FF", textIcon: "AA", website: "https://assemblyai.com" },
|
||||||
|
ollama: { id: "ollama", alias: "ollama", name: "Ollama Cloud", icon: "cloud", color: "#ffffffff", textIcon: "OL", website: "https://ollama.com" },
|
||||||
nanobanana: { id: "nanobanana", alias: "nb", name: "NanoBanana", icon: "image", color: "#FFD700", textIcon: "NB", website: "https://nanobananaapi.ai" },
|
nanobanana: { id: "nanobanana", alias: "nb", name: "NanoBanana", icon: "image", color: "#FFD700", textIcon: "NB", website: "https://nanobananaapi.ai" },
|
||||||
chutes: { id: "chutes", alias: "ch", name: "Chutes AI", icon: "water_drop", color: "#5B6EF5", textIcon: "CH", website: "https://chutes.ai" },
|
chutes: { id: "chutes", alias: "ch", name: "Chutes AI", icon: "water_drop", color: "#ffffffff", textIcon: "CH", website: "https://chutes.ai" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
||||||
|
|||||||
118
tests/unit/translator-request-normalization.test.js
Normal file
118
tests/unit/translator-request-normalization.test.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||||
|
import { translateRequest } from "../../open-sse/translator/index.js";
|
||||||
|
import { claudeToOpenAIRequest } from "../../open-sse/translator/request/claude-to-openai.js";
|
||||||
|
import { filterToOpenAIFormat } from "../../open-sse/translator/helpers/openaiHelper.js";
|
||||||
|
import { parseSSELine } from "../../open-sse/utils/streamHelpers.js";
|
||||||
|
|
||||||
|
describe("request normalization", () => {
|
||||||
|
it("claudeToOpenAIRequest flattens text-only content arrays into string", () => {
|
||||||
|
const body = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "hi" },
|
||||||
|
{ type: "text", text: "there" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = claudeToOpenAIRequest("gpt-oss:120b", body, true);
|
||||||
|
expect(result.messages[0].content).toBe("hi\nthere");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("claudeToOpenAIRequest preserves multimodal arrays", () => {
|
||||||
|
const body = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "describe" },
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
source: {
|
||||||
|
type: "base64",
|
||||||
|
media_type: "image/png",
|
||||||
|
data: "ZmFrZQ==",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = claudeToOpenAIRequest("gpt-4o", body, true);
|
||||||
|
expect(Array.isArray(result.messages[0].content)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filterToOpenAIFormat flattens text-only arrays to string", () => {
|
||||||
|
const body = {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "a" },
|
||||||
|
{ type: "text", text: "b" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = filterToOpenAIFormat(JSON.parse(JSON.stringify(body)));
|
||||||
|
expect(result.messages[0].content).toBe("a\nb");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("translateRequest keeps /v1/messages Claude->OpenAI text payloads string-safe", () => {
|
||||||
|
const body = {
|
||||||
|
model: "ollama/gpt-oss:120b",
|
||||||
|
system: [{ type: "text", text: "You are helpful." }],
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "hello" },
|
||||||
|
{ type: "text", text: "world" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stream: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = translateRequest(
|
||||||
|
FORMATS.CLAUDE,
|
||||||
|
FORMATS.OPENAI,
|
||||||
|
"gpt-oss:120b",
|
||||||
|
JSON.parse(JSON.stringify(body)),
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
"ollama",
|
||||||
|
);
|
||||||
|
|
||||||
|
const userMessage = result.messages.find((m) => m.role === "user");
|
||||||
|
expect(typeof userMessage.content).toBe("string");
|
||||||
|
expect(userMessage.content).toBe("hello\nworld");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parseSSELine supports provider raw NDJSON stream lines", () => {
|
||||||
|
const raw = JSON.stringify({
|
||||||
|
model: "gpt-oss:120b",
|
||||||
|
message: { role: "assistant", content: "hello" },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = parseSSELine(raw);
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
model: "gpt-oss:120b",
|
||||||
|
message: { role: "assistant", content: "hello" },
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parseSSELine still supports SSE data lines", () => {
|
||||||
|
const parsed = parseSSELine('data: {"choices":[{"delta":{"content":"hi"}}]}');
|
||||||
|
expect(parsed.choices[0].delta.content).toBe("hi");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user