mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
refactor(open-sse): translator DRY + schema enums, bug fixes, dead code cleanup
- Bug B1-B7: media UI m.kind||m.type, serviceKinds, gemini mediaPriority, schema kind, models/info lookup by kind - Dead code D1-D6: safeParseJSON, drop PROVIDER_ENDPOINTS, orphan fetcher, GITHUB_CONFIG derive, getProviderConfig internal, legacy kiro file - Translator concerns: toOpenAIUsage, toOpenAIFinish (gemini/kiro/ollama + fix kiro tool finish), thinking effort maps - Reorg helpers/ → concerns/ (logic) + formats/ (per-format) + schema/ (pure enums: roles/blocks/finishReasons/defaults) - Wire ~280 hardcoded role/block/finish/default literals to schema enums across 20+ files - collapseTextParts + extractTextContent dedup - Normalize translator fn names to openaiToXRequest / xToOpenAIResponse - Golden tests lock behavior; 0 regression (byte-for-byte providers/alias, 26=26 known fails) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -10,7 +10,7 @@ const baseline = JSON.parse(readFileSync(join(here, "providers-baseline.json"),
|
||||
|
||||
// Fields intentionally added during refactor (verified by dedicated runtime tests, not byte-baseline).
|
||||
// authUrl: removed dead field (qwen/iflow) — no consumer reads config.authUrl (oauth block has authorize/deviceCode)
|
||||
const ADDED_FIELDS = new Set(["forceStream", "urlSuffix", "retry", "quirks", "auth", "validateUrl", "usage", "clientId", "clientSecret", "tokenUrl", "cliVersion", "apiClient", "copilot", "authorizeUrl", "authUrl", "regions", "defaultRegion", "reasoningInject"]);
|
||||
const ADDED_FIELDS = new Set(["forceStream", "urlSuffix", "retry", "quirks", "auth", "validateUrl", "usage", "clientId", "clientSecret", "tokenUrl", "cliVersion", "apiClient", "copilot", "authorizeUrl", "authUrl", "regions", "defaultRegion", "reasoningInject", "priority", "hasFree"]);
|
||||
|
||||
// Normalize via JSON roundtrip so function/undefined are dropped identically; drop added/removed fields.
|
||||
// ADDED_FIELDS are verified by dedicated runtime tests, so drop them from BOTH sides (added or intentionally removed).
|
||||
|
||||
@@ -14,7 +14,9 @@ Components:
|
||||
- `formats.js` — `FORMATS` enum (openai, claude, gemini, gemini-cli, openai-responses, antigravity, kiro, cursor, commandcode, ollama, vertex).
|
||||
- `request/<from>-to-<to>.js` — one-way request translation.
|
||||
- `response/<from>-to-<to>.js` — one-way SSE response translation.
|
||||
- `helpers/` — `openaiHelper.js` (filterToOpenAIFormat), `toolCallHelper.js` (id/arguments), `claudeHelper.js`, `geminiHelper.js`.
|
||||
- `schema/` — pure data enums (no logic): `roles.js` (ROLE, GEMINI_ROLE), `blocks.js` (OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM, valid-type lists), `finishReasons.js` (OPENAI_FINISH, CLAUDE_STOP, GEMINI_FINISH), `defaults.js` (MODEL_FALLBACK, DEFAULT_IMAGE_MIME). Import via `schema/index.js`.
|
||||
- `concerns/` — cross-format translation LOGIC: `chunk.js`, `usage.js`, `reasoning.js`, `thinking.js` (effort↔budget/level), `toolCall.js`, `finishReason.js` (mapping fns), `image.js`, `json.js`.
|
||||
- `formats/` — per-format logic: `openai.js` (filterToOpenAIFormat), `claude.js`, `gemini.js`, `responsesApi.js`, `maxTokens.js`.
|
||||
|
||||
**OpenAI-bridge pitfalls** (source of most bugs): going through OpenAI easily loses `thinking`/`reasoning`, image URLs (non-base64), `input_audio`, `is_error`; tool `id`/`index` become unstable (parallel tool calls), non-text system blocks, `tool_choice:"none"`.
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ exports[`GOLDEN response stream: Kiro → OpenAI > text + reasoning + toolUse +
|
||||
"choices": [
|
||||
{
|
||||
"delta": {},
|
||||
"finish_reason": "stop",
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`GOLDEN response stream: CommandCode → OpenAI > text + reasoning + tool + finish-step usage 1`] = `
|
||||
[
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Hello",
|
||||
"role": "assistant",
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "commandcode",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"reasoning_content": "thinking",
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "commandcode",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "",
|
||||
"name": "get_weather",
|
||||
},
|
||||
"id": "t1",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "commandcode",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{"city":"NYC"}",
|
||||
},
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "commandcode",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {},
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "commandcode",
|
||||
"object": "chat.completion.chunk",
|
||||
"usage": {
|
||||
"completion_tokens": 5,
|
||||
"prompt_tokens": 10,
|
||||
"total_tokens": 15,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`GOLDEN response stream: Kiro → OpenAI (finish after tool) > toolUse then stop — lock current finish_reason behavior 1`] = `
|
||||
[
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Hi",
|
||||
"role": "assistant",
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "kiro",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{"q":"x"}",
|
||||
"name": "search",
|
||||
},
|
||||
"id": "tu_1",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "kiro",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {},
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "kiro",
|
||||
"object": "chat.completion.chunk",
|
||||
"usage": {
|
||||
"completion_tokens": 3,
|
||||
"prompt_tokens": 7,
|
||||
"total_tokens": 10,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`GOLDEN response stream: Ollama → OpenAI (finish after tool) > tool_calls then done_reason=stop — lock current finish_reason 1`] = `
|
||||
[
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{"q":"x"}",
|
||||
"name": "search",
|
||||
},
|
||||
"id": "call_0_<TS>",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "qwen3",
|
||||
"object": "chat.completion.chunk",
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"delta": {},
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
"created": 0,
|
||||
"id": "chatcmpl-<TS>",
|
||||
"model": "qwen3",
|
||||
"object": "chat.completion.chunk",
|
||||
"usage": {
|
||||
"completion_tokens": 2,
|
||||
"prompt_tokens": 5,
|
||||
"total_tokens": 7,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`GOLDEN usage math: Claude prompt = input + cache (lock) > prompt_tokens sums input + cache_read + cache_creation 1`] = `
|
||||
{
|
||||
"completion_tokens": 5,
|
||||
"prompt_tokens": 15,
|
||||
"prompt_tokens_details": {
|
||||
"cache_creation_tokens": 2,
|
||||
"cached_tokens": 3,
|
||||
},
|
||||
"total_tokens": 20,
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,106 @@
|
||||
// P0 GOLDEN (refactor2): lock OUTPUT translateResponse/Request cho các concern
|
||||
// SẮP refactor ở P1-P4 (usage field-map, finishReasonMap, reasoningDelta).
|
||||
// Bổ sung coverage còn thiếu so với golden-response-stream.test.js:
|
||||
// - commandcode usage/finish
|
||||
// - passthrough openai→openai (request + response)
|
||||
// - kiro/ollama finish_reason sau tool (lock behavior HIỆN TẠI, kể cả bug đã biết)
|
||||
// Sau refactor chạy lại phải khớp y hệt. Lệch = regression.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import "./registerAll.js";
|
||||
import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
|
||||
// Strip volatile id/created so snapshots are stable across runs.
|
||||
function stripVolatile(chunks) {
|
||||
return JSON.parse(JSON.stringify(chunks), (key, val) => {
|
||||
if (key === "created") return 0;
|
||||
if (key === "id" && typeof val === "string") {
|
||||
return val
|
||||
.replace(/-\d{10,}-(\d+)$/, "-<TS>-$1")
|
||||
.replace(/^chatcmpl-\d{10,}$/, "chatcmpl-<TS>")
|
||||
.replace(/^call_(\d+)_\d{10,}$/, "call_$1_<TS>")
|
||||
.replace(/^call_\d{10,}_(\d+)$/, "call_<TS>_$1");
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
function runStream(targetFormat, sourceFormat, events) {
|
||||
const state = initState(sourceFormat);
|
||||
const all = [];
|
||||
for (const ev of events) {
|
||||
const out = translateResponse(targetFormat, sourceFormat, ev, state);
|
||||
if (Array.isArray(out)) all.push(...out);
|
||||
else if (out) all.push(out);
|
||||
}
|
||||
return stripVolatile(all);
|
||||
}
|
||||
|
||||
describe("GOLDEN response stream: CommandCode → OpenAI", () => {
|
||||
it("text + reasoning + tool + finish-step usage", () => {
|
||||
const events = [
|
||||
{ type: "text-delta", text: "Hello" },
|
||||
{ type: "reasoning-delta", text: "thinking" },
|
||||
{ type: "tool-input-start", id: "t1", toolName: "get_weather" },
|
||||
{ type: "tool-input-delta", id: "t1", delta: '{"city":"NYC"}' },
|
||||
{ type: "finish-step", finishReason: "tool-calls", usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 } },
|
||||
{ type: "finish" },
|
||||
];
|
||||
expect(runStream(FORMATS.COMMANDCODE, FORMATS.OPENAI, events)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GOLDEN response stream: Kiro → OpenAI (finish after tool)", () => {
|
||||
it("toolUse then stop — lock current finish_reason behavior", () => {
|
||||
const events = [
|
||||
{ assistantResponseEvent: { content: "Hi" }, _eventType: "assistantResponseEvent" },
|
||||
{ toolUseEvent: { toolUseId: "tu_1", name: "search", input: { q: "x" } }, _eventType: "toolUseEvent" },
|
||||
{ usageEvent: { inputTokens: 7, outputTokens: 3 }, _eventType: "usageEvent" },
|
||||
{ _eventType: "messageStopEvent" },
|
||||
];
|
||||
expect(runStream(FORMATS.KIRO, FORMATS.OPENAI, events)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GOLDEN response stream: Ollama → OpenAI (finish after tool)", () => {
|
||||
it("tool_calls then done_reason=stop — lock current finish_reason", () => {
|
||||
const events = [
|
||||
{ model: "qwen3", message: { role: "assistant", tool_calls: [{ function: { name: "search", arguments: { q: "x" } } }] } },
|
||||
{ model: "qwen3", done: true, done_reason: "stop", prompt_eval_count: 5, eval_count: 2 },
|
||||
];
|
||||
expect(runStream(FORMATS.OLLAMA, FORMATS.OPENAI, events)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GOLDEN passthrough: same format = no translation", () => {
|
||||
it("response openai→openai returns chunk unchanged", () => {
|
||||
const chunk = { id: "chatcmpl-x", object: "chat.completion.chunk", created: 1, model: "gpt-4o", choices: [{ index: 0, delta: { content: "hi" }, finish_reason: null }] };
|
||||
const state = initState(FORMATS.OPENAI);
|
||||
const out = translateResponse(FORMATS.OPENAI, FORMATS.OPENAI, chunk, state);
|
||||
expect(out).toEqual([chunk]);
|
||||
});
|
||||
|
||||
it("request openai→openai keeps messages (filterToOpenAIFormat normalize)", () => {
|
||||
const body = { model: "gpt-4o", messages: [{ role: "user", content: "hi" }] };
|
||||
const out = translateRequest(FORMATS.OPENAI, FORMATS.OPENAI, "gpt-4o", body, true);
|
||||
expect(out.messages).toEqual([{ role: "user", content: "hi" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GOLDEN usage math: Claude prompt = input + cache (lock)", () => {
|
||||
it("prompt_tokens sums input + cache_read + cache_creation", () => {
|
||||
const events = [
|
||||
{ type: "message_start", message: { id: "msg_1", model: "claude-opus-4-6" } },
|
||||
{ type: "content_block_start", index: 0, content_block: { type: "text" } },
|
||||
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "ok" } },
|
||||
{ type: "content_block_stop", index: 0 },
|
||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 3, cache_creation_input_tokens: 2 } },
|
||||
{ type: "message_stop" },
|
||||
];
|
||||
const out = runStream(FORMATS.CLAUDE, FORMATS.OPENAI, events);
|
||||
const finalChunk = out.find(c => c.usage);
|
||||
expect(finalChunk.usage.prompt_tokens).toBe(15); // 10 + 3 + 2
|
||||
expect(finalChunk.usage.completion_tokens).toBe(5);
|
||||
expect(finalChunk.usage).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -9,13 +9,13 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { convertCommandCodeToOpenAI } from "../../open-sse/translator/response/commandcode-to-openai.js";
|
||||
import { commandCodeToOpenAIResponse } from "../../open-sse/translator/response/commandcode-to-openai.js";
|
||||
|
||||
function feed(events) {
|
||||
const state = {};
|
||||
const all = [];
|
||||
for (const e of events) {
|
||||
const out = convertCommandCodeToOpenAI(JSON.stringify(e), state);
|
||||
const out = commandCodeToOpenAIResponse(JSON.stringify(e), state);
|
||||
if (out) for (const c of out) all.push(c);
|
||||
}
|
||||
return { state, chunks: all };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Locks multimodal quirks flagged in docs 11 §4: image_url.detail drop + input_audio per-format.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.js";
|
||||
import { convertOpenAIContentToParts } from "../../open-sse/translator/helpers/geminiHelper.js";
|
||||
import { convertOpenAIContentToParts } from "../../open-sse/translator/formats/gemini.js";
|
||||
|
||||
function userImage(detail) {
|
||||
return {
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { openaiToCommandCode } from "../../open-sse/translator/request/openai-to-commandcode.js";
|
||||
import { openaiToCommandCodeRequest } from "../../open-sse/translator/request/openai-to-commandcode.js";
|
||||
|
||||
const MODEL = "moonshotai/Kimi-K2.6";
|
||||
|
||||
describe("openaiToCommandCode — basic envelope", () => {
|
||||
describe("openaiToCommandCodeRequest — basic envelope", () => {
|
||||
it("returns the expected top-level envelope shape", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
}, true);
|
||||
|
||||
@@ -28,9 +28,9 @@ describe("openaiToCommandCode — basic envelope", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("openaiToCommandCode — system handling", () => {
|
||||
describe("openaiToCommandCodeRequest — system handling", () => {
|
||||
it("hoists system messages to params.system (string), not messages[]", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [
|
||||
{ role: "system", content: "You are concise." },
|
||||
{ role: "user", content: "hi" },
|
||||
@@ -44,7 +44,7 @@ describe("openaiToCommandCode — system handling", () => {
|
||||
});
|
||||
|
||||
it("joins multiple system messages with blank line", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [
|
||||
{ role: "system", content: "A" },
|
||||
{ role: "system", content: "B" },
|
||||
@@ -56,16 +56,16 @@ describe("openaiToCommandCode — system handling", () => {
|
||||
});
|
||||
|
||||
it("omits params.system when no system messages", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
}, true);
|
||||
expect(out.params.system).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openaiToCommandCode — content shape", () => {
|
||||
describe("openaiToCommandCodeRequest — content shape", () => {
|
||||
it("MUST always emit content as Array (never string) for user", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
}, true);
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("openaiToCommandCode — content shape", () => {
|
||||
});
|
||||
|
||||
it("MUST always emit content as Array for assistant", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [
|
||||
{ role: "user", content: "a" },
|
||||
{ role: "assistant", content: "b" },
|
||||
@@ -87,9 +87,9 @@ describe("openaiToCommandCode — content shape", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("openaiToCommandCode — tool role / tool-result (AI SDK)", () => {
|
||||
describe("openaiToCommandCodeRequest — tool role / tool-result (AI SDK)", () => {
|
||||
it("converts role:\"tool\" to role:\"tool\" with tool-result block; output is {type:\"text\",value}", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [
|
||||
{ role: "user", content: "run X" },
|
||||
{
|
||||
@@ -113,9 +113,9 @@ describe("openaiToCommandCode — tool role / tool-result (AI SDK)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("openaiToCommandCode — assistant tool_calls / tool-call", () => {
|
||||
describe("openaiToCommandCodeRequest — assistant tool_calls / tool-call", () => {
|
||||
it("converts assistant.tool_calls[] into content blocks of type tool-call", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [
|
||||
{ role: "user", content: "go" },
|
||||
{
|
||||
@@ -138,9 +138,9 @@ describe("openaiToCommandCode — assistant tool_calls / tool-call", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("openaiToCommandCode — tools schema conversion", () => {
|
||||
describe("openaiToCommandCodeRequest — tools schema conversion", () => {
|
||||
it("converts OpenAI {type:\"function\", function:{...}} to Anthropic plain {name, input_schema}", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [
|
||||
{
|
||||
@@ -163,7 +163,7 @@ describe("openaiToCommandCode — tools schema conversion", () => {
|
||||
});
|
||||
|
||||
it("preserves description on converted tool", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
tools: [
|
||||
{ type: "function", function: { name: "ping", description: "Ping the server", parameters: { type: "object" } } },
|
||||
@@ -173,7 +173,7 @@ describe("openaiToCommandCode — tools schema conversion", () => {
|
||||
});
|
||||
|
||||
it("does not include tools field when input has none", () => {
|
||||
const out = openaiToCommandCode(MODEL, {
|
||||
const out = openaiToCommandCodeRequest(MODEL, {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
}, true);
|
||||
expect(out.params.tools).toBeUndefined();
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
* Unit tests for open-sse/translator/request/openai-to-kiro.js
|
||||
*
|
||||
* Tests cover:
|
||||
* - buildKiroPayload() - basic message conversion
|
||||
* - openaiToKiroRequest() - basic message conversion
|
||||
* - Image forwarding fix: images in currentMessage must be included in payload
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.js";
|
||||
import { openaiToKiroRequest } from "../../open-sse/translator/request/openai-to-kiro.js";
|
||||
|
||||
describe("buildKiroPayload", () => {
|
||||
describe("openaiToKiroRequest", () => {
|
||||
describe("basic message conversion", () => {
|
||||
it("should convert a simple text message", () => {
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Hello" }]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
expect(currentMsg.userInputMessage.content).toContain("Hello");
|
||||
@@ -29,7 +29,7 @@ describe("buildKiroPayload", () => {
|
||||
messages: [{ role: "user", content: "No images here" }]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
expect(currentMsg.userInputMessage.images).toBeUndefined();
|
||||
@@ -51,7 +51,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
expect(currentMsg.userInputMessage.images).toBeDefined();
|
||||
@@ -75,7 +75,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
expect(currentMsg.userInputMessage.images).toHaveLength(2);
|
||||
@@ -95,7 +95,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
expect(currentMsg.userInputMessage.images).toBeUndefined();
|
||||
@@ -115,7 +115,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
expect(currentMsg.userInputMessage.content).toContain("What is in this image?");
|
||||
@@ -135,7 +135,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
|
||||
const currentMsg = result.conversationState.currentMessage;
|
||||
// HTTP URLs are not supported by Kiro — converted to text placeholder
|
||||
@@ -166,7 +166,7 @@ describe("buildKiroPayload", () => {
|
||||
// note: no `tools`
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
const cs = result.conversationState;
|
||||
|
||||
// No structured tool content anywhere
|
||||
@@ -201,7 +201,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
const cs = result.conversationState;
|
||||
|
||||
const allJson = JSON.stringify(cs);
|
||||
@@ -233,7 +233,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
const cs = result.conversationState;
|
||||
|
||||
// Structured tool spec carried on currentMessage
|
||||
@@ -270,7 +270,7 @@ describe("buildKiroPayload", () => {
|
||||
]
|
||||
};
|
||||
|
||||
const result = buildKiroPayload("claude-sonnet-4.6", body, true, {});
|
||||
const result = openaiToKiroRequest("claude-sonnet-4.6", body, true, {});
|
||||
const cs = result.conversationState;
|
||||
const allJson = JSON.stringify(cs);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Locks edge cases flagged in docs 11 §1/§4 that were only covered indirectly.
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { normalizeClaudePassthrough } from "../../open-sse/translator/helpers/claudeHelper.js";
|
||||
import { parseDataUri, encodeDataUri } from "../../open-sse/translator/helpers/imageHelper.js";
|
||||
import { normalizeClaudePassthrough } from "../../open-sse/translator/formats/claude.js";
|
||||
import { parseDataUri, encodeDataUri } from "../../open-sse/translator/concerns/image.js";
|
||||
|
||||
describe("normalizeClaudePassthrough — haiku adaptive thinking (docs 11 §1)", () => {
|
||||
it("downgrades adaptive thinking to enabled+budget for haiku models", () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { filterToOpenAIFormat } from "../../open-sse/translator/formats/openai.js";
|
||||
import { parseSSELine } from "../../open-sse/utils/streamHelpers.js";
|
||||
|
||||
describe("request normalization", () => {
|
||||
|
||||
Reference in New Issue
Block a user