mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
fix(reasoning): forward Ollama Cloud thinking (#9290)
This commit is contained in:
@@ -12,6 +12,18 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
// Note: rate limits vary by plan (free = "Light usage", Pro = more, Max = 5x Pro).
|
||||
// Users can generate API keys at https://ollama.com/settings/keys
|
||||
models: [
|
||||
{
|
||||
id: "gpt-oss:20b",
|
||||
name: "GPT-OSS 20B",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high"],
|
||||
},
|
||||
{
|
||||
id: "gpt-oss:120b",
|
||||
name: "GPT-OSS 120B",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high"],
|
||||
},
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6" },
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface RegistryModel {
|
||||
aliases?: readonly string[];
|
||||
toolCalling?: boolean;
|
||||
supportsReasoning?: boolean;
|
||||
supportedThinkingEfforts?: readonly string[];
|
||||
supportsVision?: boolean;
|
||||
supportsXHighEffort?: boolean;
|
||||
maxOutputTokens?: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { appendToolCallArgumentDelta } from "../utils/toolCallArguments.ts";
|
||||
import { shouldParseTextualReasoningTags } from "../handlers/responseSanitizer.ts";
|
||||
import { getReadableReasoningValue } from "../utils/reasoningFields.ts";
|
||||
import {
|
||||
isInternalReasoningPlaceholder,
|
||||
stripInternalReasoningPlaceholder,
|
||||
@@ -528,10 +529,13 @@ export function createResponsesApiTransformStream(
|
||||
});
|
||||
}
|
||||
|
||||
// Handle reasoning_content (OpenAI native format)
|
||||
if (delta.reasoning_content && !isInternalReasoningPlaceholder(delta.reasoning_content)) {
|
||||
// Handle OpenAI-compatible reasoning fields. Some providers use the
|
||||
// standard `reasoning_content` key while others use the string alias
|
||||
// `reasoning`; prefer the standard key when both are present.
|
||||
const reasoning = getReadableReasoningValue(delta);
|
||||
if (reasoning && !isInternalReasoningPlaceholder(reasoning)) {
|
||||
startReasoning(controller, idx);
|
||||
emitReasoningDelta(controller, delta.reasoning_content);
|
||||
emitReasoningDelta(controller, reasoning);
|
||||
}
|
||||
|
||||
// Handle text content. Generic prompt-format tags are visible text;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FORMATS } from "../formats.ts";
|
||||
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
|
||||
import { fallbackToolCallId } from "../helpers/toolCallHelper.ts";
|
||||
import { shouldParseTextualReasoningTags } from "../../handlers/responseSanitizer.ts";
|
||||
import { getReadableReasoningValue } from "../../utils/reasoningFields.ts";
|
||||
import {
|
||||
isInternalReasoningPlaceholder,
|
||||
stripInternalReasoningPlaceholder,
|
||||
@@ -80,9 +81,7 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {
|
||||
return flushEvents(state);
|
||||
}
|
||||
|
||||
// Capture usage from all chunks that carry it (usage-only chunks OR final chunks with finish_reason)
|
||||
// Normalize Chat Completions format (prompt_tokens/completion_tokens) to Responses API format
|
||||
// (input_tokens/output_tokens) so response.completed always has the fields Codex expects.
|
||||
// Normalize usage from any chunk so response.completed has Responses token fields.
|
||||
if (chunk.usage) {
|
||||
const u = chunk.usage;
|
||||
const input_tokens = u.input_tokens ?? u.prompt_tokens ?? 0;
|
||||
@@ -193,9 +192,10 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {
|
||||
});
|
||||
}
|
||||
|
||||
if (delta.reasoning_content && !isInternalReasoningPlaceholder(delta.reasoning_content)) {
|
||||
const reasoning = getReadableReasoningValue(delta);
|
||||
if (reasoning && !isInternalReasoningPlaceholder(reasoning)) {
|
||||
startReasoning(state, emit, idx);
|
||||
emitReasoningDelta(state, emit, delta.reasoning_content);
|
||||
emitReasoningDelta(state, emit, reasoning);
|
||||
}
|
||||
// Strip the internal reasoning placeholder if the model echoed it
|
||||
// through ordinary content (#8081). Only the text-content emission is
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CORS_HEADERS } from "./cors.ts";
|
||||
import { getReadableReasoningValue } from "./reasoningFields.ts";
|
||||
|
||||
type PendingToolCall = {
|
||||
id?: string;
|
||||
@@ -38,6 +39,7 @@ export function transformToOllama(response, model) {
|
||||
const parsed = JSON.parse(data);
|
||||
const delta = parsed.choices?.[0]?.delta || {};
|
||||
const content = delta.content || "";
|
||||
const thinking = getReadableReasoningValue(delta);
|
||||
const toolCalls = delta.tool_calls;
|
||||
|
||||
if (toolCalls) {
|
||||
@@ -47,7 +49,11 @@ export function transformToOllama(response, model) {
|
||||
const toolCallId = tc.id != null ? String(tc.id) : tc.id;
|
||||
|
||||
// T37: Prevent merging tool_calls on same index if ID changes
|
||||
if (pendingToolCalls[idx] && toolCallId && pendingToolCalls[idx].id !== toolCallId) {
|
||||
if (
|
||||
pendingToolCalls[idx] &&
|
||||
toolCallId &&
|
||||
pendingToolCalls[idx].id !== toolCallId
|
||||
) {
|
||||
completedToolCalls.push(pendingToolCalls[idx]);
|
||||
delete pendingToolCalls[idx];
|
||||
}
|
||||
@@ -64,6 +70,16 @@ export function transformToOllama(response, model) {
|
||||
}
|
||||
}
|
||||
|
||||
if (thinking) {
|
||||
const ollama =
|
||||
JSON.stringify({
|
||||
model,
|
||||
message: { role: "assistant", content: "", thinking },
|
||||
done: false,
|
||||
}) + "\n";
|
||||
controller.enqueue(new TextEncoder().encode(ollama));
|
||||
}
|
||||
|
||||
if (content) {
|
||||
const ollama =
|
||||
JSON.stringify({ model, message: { role: "assistant", content }, done: false }) +
|
||||
|
||||
@@ -461,7 +461,12 @@ async function buildUnifiedModelsResponseCore(
|
||||
}
|
||||
Object.assign(
|
||||
capabilities,
|
||||
getThinkingCapabilityFields(providerId, modelId, canonical.capabilities.supportsThinking)
|
||||
getThinkingCapabilityFields(
|
||||
providerId,
|
||||
modelId,
|
||||
canonical.capabilities.supportsThinking,
|
||||
registryModel?.supportedThinkingEfforts
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -83,7 +83,8 @@ export function minKnownNumber(values: Array<number | undefined>): number | unde
|
||||
export function getThinkingCapabilityFields(
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
resolvedThinking?: boolean | null
|
||||
resolvedThinking?: boolean | null,
|
||||
supportedThinkingEfforts?: readonly string[]
|
||||
): Record<string, boolean | string[]> {
|
||||
const supportsThinking = resolvedThinking;
|
||||
if (typeof supportsThinking !== "boolean") return {};
|
||||
@@ -92,7 +93,10 @@ export function getThinkingCapabilityFields(
|
||||
supportsThinking,
|
||||
...(supportsThinking
|
||||
? {
|
||||
effort_tiers: extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES),
|
||||
effort_tiers:
|
||||
supportedThinkingEfforts && supportedThinkingEfforts.length > 0
|
||||
? [...supportedThinkingEfforts]
|
||||
: extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
@@ -2,10 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { parseModel } from "@omniroute/open-sse/services/model.ts";
|
||||
import { getModelInfo } from "@/sse/services/model";
|
||||
import { getModelAliases } from "@/lib/db/models";
|
||||
import {
|
||||
getResolvedModelCapabilities,
|
||||
isNonChatCatalogSurface,
|
||||
} from "@/lib/modelCapabilities";
|
||||
import { getResolvedModelCapabilities, isNonChatCatalogSurface } from "@/lib/modelCapabilities";
|
||||
import {
|
||||
getAuthoritativeContextWindow,
|
||||
getAuthoritativeProviderContextWindow,
|
||||
@@ -346,6 +343,10 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
|
||||
|
||||
const metadata = getCanonicalModelMetadata({ provider, model });
|
||||
if (!metadata) return entry;
|
||||
const registryModel = getRegistryModel(
|
||||
metadata.providerAlias || metadata.provider,
|
||||
metadata.model
|
||||
);
|
||||
|
||||
const nextEntry: JsonRecord = { ...entry };
|
||||
const existingName = asNonEmptyString(entry.name);
|
||||
@@ -382,11 +383,15 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
|
||||
supportsThinking: metadata.capabilities.supportsThinking,
|
||||
...(metadata.capabilities.supportsThinking
|
||||
? {
|
||||
effort_tiers: extendCodexGpt56EffortValues(
|
||||
metadata.provider,
|
||||
metadata.model,
|
||||
CANONICAL_EFFORT_VALUES
|
||||
),
|
||||
effort_tiers:
|
||||
registryModel?.supportedThinkingEfforts &&
|
||||
registryModel.supportedThinkingEfforts.length > 0
|
||||
? [...registryModel.supportedThinkingEfforts]
|
||||
: extendCodexGpt56EffortValues(
|
||||
metadata.provider,
|
||||
metadata.model,
|
||||
CANONICAL_EFFORT_VALUES
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type VscodeCatalogModel = {
|
||||
name?: string;
|
||||
root?: string;
|
||||
owned_by?: string;
|
||||
capabilities?: Record<string, boolean>;
|
||||
capabilities?: Record<string, boolean | string[]>;
|
||||
supportsReasoningEffort?: string[];
|
||||
supportedReasoningEfforts?: string[];
|
||||
supports_reasoning_effort?: string[];
|
||||
@@ -66,6 +66,9 @@ function normalizeReasoningEffortValue(value: string) {
|
||||
|
||||
function getNativeReasoningEffortValues(model: VscodeCatalogModel) {
|
||||
const candidates = [
|
||||
model.owned_by !== "combo" && Array.isArray(model.capabilities?.effort_tiers)
|
||||
? model.capabilities.effort_tiers
|
||||
: undefined,
|
||||
model.supportsReasoningEffort,
|
||||
model.supportedReasoningEfforts,
|
||||
model.supports_reasoning_effort,
|
||||
@@ -111,7 +114,7 @@ export function getReasoningEffortValues(model: VscodeCatalogModel) {
|
||||
if (!isReasoningCapableModel(model)) return undefined;
|
||||
|
||||
const modelId = getCatalogModelName(model);
|
||||
const parsed = parseModel(modelId, "");
|
||||
const parsed = parseModel(modelId);
|
||||
const providerId = parsed.provider || model.owned_by || "";
|
||||
const providerModelId = parsed.model || model.root || modelId.split("/").pop() || modelId;
|
||||
const values = ["none", "low", "medium", "high"];
|
||||
@@ -179,7 +182,7 @@ export function getReasoningVariantBaseModelId(modelId: string) {
|
||||
|
||||
function getCodexGpt56DefaultReasoningEffort(model: VscodeCatalogModel) {
|
||||
const modelId = getCatalogModelName(model);
|
||||
const parsed = parseModel(modelId, "");
|
||||
const parsed = parseModel(modelId);
|
||||
const providerId = (parsed.provider || model.owned_by || "").trim().toLowerCase();
|
||||
if (providerId !== "codex" && providerId !== "cx") return undefined;
|
||||
|
||||
@@ -194,9 +197,18 @@ function getCodexGpt56DefaultReasoningEffort(model: VscodeCatalogModel) {
|
||||
}
|
||||
|
||||
export function getDefaultReasoningEffort(model: VscodeCatalogModel, supportedValues?: string[]) {
|
||||
const nativeDefault = normalizeReasoningEffortValue(
|
||||
model.defaultReasoningEffort || model.default_reasoning_effort || ""
|
||||
);
|
||||
return (
|
||||
inferSelectedReasoningEffort(model, supportedValues) ||
|
||||
(nativeDefault && (!supportedValues?.length || supportedValues.includes(nativeDefault))
|
||||
? nativeDefault
|
||||
: undefined) ||
|
||||
getCodexGpt56DefaultReasoningEffort(model) ||
|
||||
(supportedValues?.includes(DEFAULT_REASONING_EFFORT)
|
||||
? DEFAULT_REASONING_EFFORT
|
||||
: supportedValues?.[0]) ||
|
||||
DEFAULT_REASONING_EFFORT
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,18 +10,22 @@ test("transformToOllama coerces numeric tool_call id to string without crashing"
|
||||
object: "chat.completion.chunk",
|
||||
created: 1,
|
||||
model: "gpt-4",
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [{
|
||||
index: 0,
|
||||
id: 12345,
|
||||
type: "function",
|
||||
function: { name: "test", arguments: "{}" }
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: 12345,
|
||||
type: "function",
|
||||
function: { name: "test", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
finish_reason: "tool_calls"
|
||||
}]
|
||||
],
|
||||
})}\n`,
|
||||
].join("");
|
||||
|
||||
@@ -87,12 +91,88 @@ test("transformToOllama handles string tool_call id normally", async () => {
|
||||
|
||||
const result = transformToOllama(mockResponse, "test-model");
|
||||
const text = await result.text();
|
||||
const lines = text.trim().split("\n").map((line) => JSON.parse(line));
|
||||
const lines = text
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
const toolCallLine = lines.find((line) => line.message?.tool_calls);
|
||||
assert.ok(toolCallLine, "Should produce a tool call line");
|
||||
});
|
||||
|
||||
test("transformToOllama emits reasoning aliases as native thinking", async () => {
|
||||
const inputSSE = [
|
||||
`data: ${JSON.stringify({
|
||||
choices: [{ index: 0, delta: { reasoning: "plan ", content: "" } }],
|
||||
})}\n`,
|
||||
`data: ${JSON.stringify({
|
||||
choices: [{ index: 0, delta: { reasoning: "carefully", content: "answer" } }],
|
||||
})}\n`,
|
||||
`data: ${JSON.stringify({
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n`,
|
||||
].join("");
|
||||
|
||||
const mockResponse = new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(inputSSE));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
|
||||
const lines = (await transformToOllama(mockResponse, "gpt-oss:20b").text())
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
const thinking = lines.filter((line) => typeof line.message?.thinking === "string");
|
||||
const content = lines.filter((line) => line.message?.content === "answer");
|
||||
|
||||
assert.deepEqual(
|
||||
thinking.map((line) => line.message.thinking),
|
||||
["plan ", "carefully"]
|
||||
);
|
||||
assert.equal(
|
||||
thinking.every((line) => line.message.content === ""),
|
||||
true
|
||||
);
|
||||
assert.equal(content.length, 1);
|
||||
assert.equal(content[0].message.thinking, undefined);
|
||||
});
|
||||
|
||||
test("transformToOllama prefers reasoning_content without duplicating aliases", async () => {
|
||||
const inputSSE = `data: ${JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: "canonical", reasoning: "alias" },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
})}\n`;
|
||||
const mockResponse = new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(inputSSE));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
|
||||
const lines = (await transformToOllama(mockResponse, "test-model").text())
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
assert.deepEqual(
|
||||
lines.filter((line) => line.message?.thinking).map((line) => line.message.thinking),
|
||||
["canonical"]
|
||||
);
|
||||
});
|
||||
|
||||
test("transformToOllama merges multi-chunk numeric tool_call id", async () => {
|
||||
const inputSSE = [
|
||||
`data: ${JSON.stringify({
|
||||
@@ -153,7 +233,10 @@ test("transformToOllama merges multi-chunk numeric tool_call id", async () => {
|
||||
|
||||
const result = transformToOllama(mockResponse, "test-model");
|
||||
const text = await result.text();
|
||||
const lines = text.trim().split("\n").map((line) => JSON.parse(line));
|
||||
const lines = text
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
const toolCallLines = lines.filter((line) => line.message?.tool_calls);
|
||||
|
||||
assert.equal(toolCallLines.length, 1);
|
||||
|
||||
@@ -38,6 +38,21 @@ test("Responses -> Chat promotes reasoning.effort for non-Copilot clients", () =
|
||||
assert.equal(out.reasoning, undefined);
|
||||
});
|
||||
|
||||
test("Responses -> Ollama Cloud Chat preserves every advertised reasoning effort", () => {
|
||||
for (const effort of ["low", "medium", "high"]) {
|
||||
const out = asRecord(
|
||||
openaiResponsesToOpenAIRequest(
|
||||
"ollama-cloud/gpt-oss:20b",
|
||||
{ input: "hello", reasoning: { effort } },
|
||||
true,
|
||||
{ _provider: "ollama-cloud" }
|
||||
)
|
||||
);
|
||||
assert.equal(out.reasoning_effort, effort);
|
||||
assert.equal(out.reasoning, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("Responses -> Chat preserves reasoning.effort via the helper wrapper", () => {
|
||||
const out = asRecord(
|
||||
convertResponsesApiFormat({ input: "hello", reasoning: { effort: "medium" } })
|
||||
|
||||
@@ -175,6 +175,48 @@ test("createResponsesApiTransformStream handles native reasoning content and too
|
||||
);
|
||||
});
|
||||
|
||||
test("createResponsesApiTransformStream converts OpenAI-compatible reasoning aliases", async () => {
|
||||
const output = await runTransformStream([
|
||||
'data: {"id":"chatcmpl_1","model":"gpt-oss:20b","choices":[{"index":0,"delta":{"reasoning":"plan "}}]}\n\n',
|
||||
'data: {"choices":[{"index":0,"delta":{"reasoning":"carefully","content":"answer"}}]}\n\n',
|
||||
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":4,"total_tokens":7}}\n\n',
|
||||
]);
|
||||
|
||||
const events = parseSseOutput(output);
|
||||
const reasoningDeltas = events
|
||||
.filter((event) => event.event === "response.reasoning_summary_text.delta")
|
||||
.map((event) => JSON.parse(event.data).delta);
|
||||
const addedItems = events
|
||||
.filter((event) => event.event === "response.output_item.added")
|
||||
.map((event) => JSON.parse(event.data).item);
|
||||
const completed = JSON.parse(
|
||||
events.find((event) => event.event === "response.completed").data
|
||||
).response;
|
||||
|
||||
assert.deepEqual(reasoningDeltas, ["plan ", "carefully"]);
|
||||
assert.deepEqual(
|
||||
addedItems.map((item) => item.type),
|
||||
["reasoning", "message"]
|
||||
);
|
||||
assert.equal(completed.output[0].type, "reasoning");
|
||||
assert.equal(completed.output[0].summary[0].text, "plan carefully");
|
||||
assert.equal(completed.output[1].content[0].text, "answer");
|
||||
});
|
||||
|
||||
test("createResponsesApiTransformStream prefers reasoning_content without duplicating aliases", async () => {
|
||||
const output = await runTransformStream([
|
||||
'data: {"choices":[{"index":0,"delta":{"reasoning_content":"canonical","reasoning":"alias"}}]}\n\n',
|
||||
'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}\n\n',
|
||||
]);
|
||||
|
||||
const events = parseSseOutput(output);
|
||||
const reasoningDeltas = events
|
||||
.filter((event) => event.event === "response.reasoning_summary_text.delta")
|
||||
.map((event) => JSON.parse(event.data).delta);
|
||||
|
||||
assert.deepEqual(reasoningDeltas, ["canonical"]);
|
||||
});
|
||||
|
||||
test("createResponsesApiTransformStream hides the internal reasoning replay placeholder", async () => {
|
||||
const output = await runTransformStream([
|
||||
'data: {"choices":[{"index":0,"delta":{"reasoning_content":"(prior reasoning summary unavailable)"}}]}\n\n',
|
||||
|
||||
@@ -18,6 +18,43 @@ function collectEvents(chunks) {
|
||||
return events;
|
||||
}
|
||||
|
||||
test("OpenAI -> Responses: accepts the reasoning alias without duplicating the canonical field", () => {
|
||||
const events = collectEvents([
|
||||
{
|
||||
id: "chatcmpl-1",
|
||||
model: "gpt-oss:20b",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning: "alias ", reasoning_content: "canonical " },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-1",
|
||||
model: "gpt-oss:20b",
|
||||
choices: [{ index: 0, delta: { reasoning: "continued" }, finish_reason: null }],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-1",
|
||||
model: "gpt-oss:20b",
|
||||
choices: [{ index: 0, delta: { content: "answer" }, finish_reason: "stop" }],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 2, total_tokens: 3 },
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
events
|
||||
.filter((event) => event.event === "response.reasoning_summary_text.delta")
|
||||
.map((event) => event.data.delta),
|
||||
["canonical ", "continued"]
|
||||
);
|
||||
const completed = events.find((event) => event.event === "response.completed").data.response;
|
||||
assert.equal(completed.output[0].summary[0].text, "canonical continued");
|
||||
assert.equal(completed.output[1].content[0].text, "answer");
|
||||
});
|
||||
|
||||
test("OpenAI -> Responses: emits lifecycle, reasoning, text, tool calls and completed usage", () => {
|
||||
const events = collectEvents([
|
||||
{
|
||||
|
||||
@@ -39,6 +39,52 @@ test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("vscode models route preserves gateway-owned Ollama Cloud effort tiers", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: true,
|
||||
password: "hashed-password",
|
||||
requireAuthForModels: true,
|
||||
});
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "ollama-cloud",
|
||||
authType: "apikey",
|
||||
name: "ollama-cloud-vscode-efforts",
|
||||
apiKey: "ollama-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
const key = await apiKeysDb.createApiKey(
|
||||
"vscode-ollama-cloud-efforts",
|
||||
"machine-vscode-ollama-cloud-efforts"
|
||||
);
|
||||
const vscodeModelsRoute = await import("../../src/app/api/v1/vscode/[token]/models/route.ts");
|
||||
|
||||
const response = await vscodeModelsRoute.GET(
|
||||
new Request(`http://localhost/api/v1/vscode/${encodeURIComponent(key.key)}/models`)
|
||||
);
|
||||
const body = (await response.json()) as {
|
||||
data?: Array<{
|
||||
id?: string;
|
||||
root?: string;
|
||||
supportsReasoningEffort?: string[];
|
||||
supportedReasoningEfforts?: string[];
|
||||
defaultReasoningEffort?: string;
|
||||
capabilities?: { effort_tiers?: string[] };
|
||||
}>;
|
||||
};
|
||||
const model = (body.data || []).find(
|
||||
(entry) => entry.root === "gpt-oss:20b" || entry.id === "ollamacloud/gpt-oss:20b"
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(model, "missing Ollama Cloud GPT-OSS model");
|
||||
assert.deepEqual(model.capabilities?.effort_tiers, ["low", "medium", "high"]);
|
||||
assert.deepEqual(model.supportsReasoningEffort, ["low", "medium", "high"]);
|
||||
assert.deepEqual(model.supportedReasoningEfforts, ["low", "medium", "high"]);
|
||||
assert.equal(model.defaultReasoningEffort, "low");
|
||||
});
|
||||
|
||||
test("vscode raw models route exposes native GPT-5.6 IDs and effort tiers", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
requireLogin: true,
|
||||
|
||||
Reference in New Issue
Block a user