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 { resolveSessionId } from "../utils/sessionManager.js";
|
||||
import { isMuseSparkModel } from "../providers/models/helpers.js";
|
||||
import { ANTHROPIC_API_VERSION } from "../providers/shared.js";
|
||||
|
||||
const OPENCODE_UA = "opencode";
|
||||
// Models served by /zen/v1/responses; every other model stays on /chat/completions.
|
||||
const RESPONSES_MODELS = new Set([
|
||||
"muse-spark-1.2-contributor-free",
|
||||
"muse-spark-1.3-contributor-free",
|
||||
]);
|
||||
const MESSAGES_MODELS = new Set(["union-alpha"]);
|
||||
|
||||
function generateRequestId() {
|
||||
return `msg_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||
@@ -31,6 +32,10 @@ function isResponsesModel(model) {
|
||||
return RESPONSES_MODELS.has(base) || isMuseSparkModel(base);
|
||||
}
|
||||
|
||||
function isMessagesModel(model) {
|
||||
return MESSAGES_MODELS.has(baseModelId(model));
|
||||
}
|
||||
|
||||
function resolveOpencodeSession(body, credentials) {
|
||||
const headers = credentials?.rawHeaders || {};
|
||||
return resolveSessionId({
|
||||
@@ -89,12 +94,12 @@ export class OpenCodeExecutor extends BaseExecutor {
|
||||
|
||||
buildUrl(model) {
|
||||
const base = this.config.baseUrl;
|
||||
return isResponsesModel(model)
|
||||
? `${base}/zen/v1/responses`
|
||||
: `${base}/zen/v1/chat/completions`;
|
||||
if (isResponsesModel(model)) return `${base}/zen/v1/responses`;
|
||||
if (isMessagesModel(model)) return `${base}/zen/v1/messages`;
|
||||
return `${base}/zen/v1/chat/completions`;
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
buildHeaders(credentials, stream = true, url = "") {
|
||||
const raw = credentials?.rawHeaders || {};
|
||||
const lower = {};
|
||||
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 isOpencodeDownstream = downstreamUa.toLowerCase().includes("opencode");
|
||||
|
||||
return {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer public",
|
||||
"User-Agent": isOpencodeDownstream ? downstreamUa : OPENCODE_UA,
|
||||
@@ -112,5 +117,7 @@ export class OpenCodeExecutor extends BaseExecutor {
|
||||
"x-opencode-project": lower["x-opencode-project"] || "global",
|
||||
"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.
|
||||
"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 },
|
||||
// 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 };
|
||||
|
||||
@@ -20,10 +20,10 @@ export default {
|
||||
noAuth: true,
|
||||
},
|
||||
models: [
|
||||
// Muse Spark models are served by /zen/v1/responses; the rest stay on
|
||||
// /chat/completions, so the format is declared per-model, not per-provider.
|
||||
// Endpoint formats differ per model, so declare non-chat models explicitly.
|
||||
{ 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: "union-alpha", name: "Union Alpha Free", targetFormat: "claude" },
|
||||
],
|
||||
modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" },
|
||||
passthroughModels: true,
|
||||
|
||||
@@ -63,6 +63,39 @@ describe("OpenCode Free Muse Spark thinking", () => {
|
||||
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", () => {
|
||||
const executor = new OpenCodeExecutor();
|
||||
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 1024 };
|
||||
|
||||
Reference in New Issue
Block a user