fix(opencode): route Union Alpha through Messages API
Route union-alpha to /zen/v1/messages with targetFormat claude, add anthropic-version header, and register model capabilities (vision, 262K context, 131K max output).
This commit is contained in:
@@ -5,13 +5,14 @@ import { getThinkingLevels } from "../providers/thinkingLevels.js";
|
|||||||
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
import { injectReasoningContent } from "../utils/reasoningContentInjector.js";
|
||||||
import { resolveSessionId } from "../utils/sessionManager.js";
|
import { resolveSessionId } from "../utils/sessionManager.js";
|
||||||
import { isMuseSparkModel } from "../providers/models/helpers.js";
|
import { isMuseSparkModel } from "../providers/models/helpers.js";
|
||||||
|
import { ANTHROPIC_API_VERSION } from "../providers/shared.js";
|
||||||
|
|
||||||
const OPENCODE_UA = "opencode";
|
const OPENCODE_UA = "opencode";
|
||||||
// Models served by /zen/v1/responses; every other model stays on /chat/completions.
|
|
||||||
const RESPONSES_MODELS = new Set([
|
const RESPONSES_MODELS = new Set([
|
||||||
"muse-spark-1.2-contributor-free",
|
"muse-spark-1.2-contributor-free",
|
||||||
"muse-spark-1.3-contributor-free",
|
"muse-spark-1.3-contributor-free",
|
||||||
]);
|
]);
|
||||||
|
const MESSAGES_MODELS = new Set(["union-alpha"]);
|
||||||
|
|
||||||
function generateRequestId() {
|
function generateRequestId() {
|
||||||
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||||
@@ -31,6 +32,10 @@ function isResponsesModel(model) {
|
|||||||
return RESPONSES_MODELS.has(base) || isMuseSparkModel(base);
|
return RESPONSES_MODELS.has(base) || isMuseSparkModel(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMessagesModel(model) {
|
||||||
|
return MESSAGES_MODELS.has(baseModelId(model));
|
||||||
|
}
|
||||||
|
|
||||||
function resolveOpencodeSession(body, credentials) {
|
function resolveOpencodeSession(body, credentials) {
|
||||||
const headers = credentials?.rawHeaders || {};
|
const headers = credentials?.rawHeaders || {};
|
||||||
return resolveSessionId({
|
return resolveSessionId({
|
||||||
@@ -89,12 +94,12 @@ export class OpenCodeExecutor extends BaseExecutor {
|
|||||||
|
|
||||||
buildUrl(model) {
|
buildUrl(model) {
|
||||||
const base = this.config.baseUrl;
|
const base = this.config.baseUrl;
|
||||||
return isResponsesModel(model)
|
if (isResponsesModel(model)) return `${base}/zen/v1/responses`;
|
||||||
? `${base}/zen/v1/responses`
|
if (isMessagesModel(model)) return `${base}/zen/v1/messages`;
|
||||||
: `${base}/zen/v1/chat/completions`;
|
return `${base}/zen/v1/chat/completions`;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildHeaders(credentials, stream = true) {
|
buildHeaders(credentials, stream = true, url = "") {
|
||||||
const raw = credentials?.rawHeaders || {};
|
const raw = credentials?.rawHeaders || {};
|
||||||
const lower = {};
|
const lower = {};
|
||||||
for (const [k, v] of Object.entries(raw)) lower[k.toLowerCase()] = v;
|
for (const [k, v] of Object.entries(raw)) lower[k.toLowerCase()] = v;
|
||||||
@@ -102,7 +107,7 @@ export class OpenCodeExecutor extends BaseExecutor {
|
|||||||
const downstreamUa = lower["user-agent"] || "";
|
const downstreamUa = lower["user-agent"] || "";
|
||||||
const isOpencodeDownstream = downstreamUa.toLowerCase().includes("opencode");
|
const isOpencodeDownstream = downstreamUa.toLowerCase().includes("opencode");
|
||||||
|
|
||||||
return {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": "Bearer public",
|
"Authorization": "Bearer public",
|
||||||
"User-Agent": isOpencodeDownstream ? downstreamUa : OPENCODE_UA,
|
"User-Agent": isOpencodeDownstream ? downstreamUa : OPENCODE_UA,
|
||||||
@@ -112,5 +117,7 @@ export class OpenCodeExecutor extends BaseExecutor {
|
|||||||
"x-opencode-project": lower["x-opencode-project"] || "global",
|
"x-opencode-project": lower["x-opencode-project"] || "global",
|
||||||
"Accept": stream ? "text/event-stream" : "*/*",
|
"Accept": stream ? "text/event-stream" : "*/*",
|
||||||
};
|
};
|
||||||
|
if (url.endsWith("/messages")) headers["anthropic-version"] = ANTHROPIC_API_VERSION;
|
||||||
|
return headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ export const MODEL_CAPABILITIES = {
|
|||||||
// via OpenAI Responses input_image; reasoning supports up to xhigh.
|
// via OpenAI Responses input_image; reasoning supports up to xhigh.
|
||||||
"muse-spark-1.2-contributor-free": { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 },
|
"muse-spark-1.2-contributor-free": { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 },
|
||||||
"muse-spark-1.3-contributor-free": { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 },
|
"muse-spark-1.3-contributor-free": { vision: true, reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 },
|
||||||
|
// OpenCode Free Union Alpha — multimodal (text+vision), 262K context, 131K max output
|
||||||
|
"union-alpha": { vision: true, contextWindow: 262144, maxOutput: 131072 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 };
|
const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 };
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export default {
|
|||||||
noAuth: true,
|
noAuth: true,
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
// Muse Spark models are served by /zen/v1/responses; the rest stay on
|
// Endpoint formats differ per model, so declare non-chat models explicitly.
|
||||||
// /chat/completions, so the format is declared per-model, not per-provider.
|
|
||||||
{ id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free", targetFormat: "openai-responses" },
|
{ id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free", targetFormat: "openai-responses" },
|
||||||
{ id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Contributor Free", targetFormat: "openai-responses" },
|
{ id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Contributor Free", targetFormat: "openai-responses" },
|
||||||
|
{ id: "union-alpha", name: "Union Alpha Free", targetFormat: "claude" },
|
||||||
],
|
],
|
||||||
modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" },
|
modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" },
|
||||||
passthroughModels: true,
|
passthroughModels: true,
|
||||||
|
|||||||
@@ -63,6 +63,39 @@ describe("OpenCode Free Muse Spark thinking", () => {
|
|||||||
expect(out.max_tokens).toBeUndefined();
|
expect(out.max_tokens).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes Union Alpha through Anthropic Messages", () => {
|
||||||
|
const caps = getCapabilitiesForModel(PROVIDER, "union-alpha");
|
||||||
|
expect(caps.vision).toBe(true);
|
||||||
|
expect(caps.contextWindow).toBe(262144);
|
||||||
|
expect(caps.maxOutput).toBe(131072);
|
||||||
|
|
||||||
|
const executor = new OpenCodeExecutor();
|
||||||
|
|
||||||
|
expect(getModelTargetFormat("oc", "union-alpha")).toBe(FORMATS.CLAUDE);
|
||||||
|
const url = executor.buildUrl("union-alpha");
|
||||||
|
expect(url).toBe("https://opencode.ai/zen/v1/messages");
|
||||||
|
expect(executor.buildHeaders({}, true, url)).toMatchObject({
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
});
|
||||||
|
expect(executor.buildHeaders({}, true, executor.buildUrl("big-pickle")))
|
||||||
|
.not.toHaveProperty("anthropic-version");
|
||||||
|
|
||||||
|
const translated = translateRequest(
|
||||||
|
FORMATS.OPENAI,
|
||||||
|
FORMATS.CLAUDE,
|
||||||
|
"union-alpha",
|
||||||
|
{ messages: [{ role: "user", content: "ping" }], max_tokens: 1 },
|
||||||
|
false,
|
||||||
|
{},
|
||||||
|
PROVIDER,
|
||||||
|
);
|
||||||
|
expect(translated).toMatchObject({
|
||||||
|
model: "union-alpha",
|
||||||
|
messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
|
||||||
|
max_tokens: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("leaves the other free models on Chat Completions", () => {
|
it("leaves the other free models on Chat Completions", () => {
|
||||||
const executor = new OpenCodeExecutor();
|
const executor = new OpenCodeExecutor();
|
||||||
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 1024 };
|
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 1024 };
|
||||||
|
|||||||
Reference in New Issue
Block a user