mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 16:22:19 +03:00
Compare commits
1 Commits
fix/9534-m
...
fix/8960-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a65cbbcee0 |
1
changelog.d/fixes/8960-fix.plan.md
Normal file
1
changelog.d/fixes/8960-fix.plan.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(opencode): propagate vision capability from live catalog into opencode.json (#8960)
|
||||
@@ -53,6 +53,9 @@ interface CatalogModelEntry {
|
||||
tool_calling?: boolean;
|
||||
vision?: boolean;
|
||||
};
|
||||
/** OpenAI-compatible modality arrays; some upstreams return these. */
|
||||
input_modalities?: string[];
|
||||
output_modalities?: string[];
|
||||
}
|
||||
|
||||
/** Per-model override carried over from the user's existing opencode.json. */
|
||||
@@ -167,6 +170,64 @@ export async function fetchOmniRouteCatalog(
|
||||
* window. The user can override per-model via `limit.context` in their
|
||||
* existing opencode.json, or fix the upstream catalog.
|
||||
*/
|
||||
/**
|
||||
* Map catalog capabilities/modalities to OpenCode model capability fields.
|
||||
* Preserves explicit user-set booleans (including `false`) over any catalog
|
||||
* value -- a deliberate local restriction must never be overwritten.
|
||||
*
|
||||
* Mapping rules per field:
|
||||
* - `attachment`: explicit user flag; then catalog `capabilities.attachment`;
|
||||
* then `capabilities.vision`; then `input_modalities` containing `image`.
|
||||
* - `reasoning`: explicit user flag; then `capabilities.reasoning`.
|
||||
* - `temperature`: explicit user flag; then `capabilities.temperature`.
|
||||
* - `tool_call`: explicit user flag; then `capabilities.tool_calling`.
|
||||
*/
|
||||
function deriveOpenCodeCapabilities(
|
||||
catalog: CatalogModelEntry | undefined,
|
||||
existing: ExistingModelEntry | undefined
|
||||
): Pick<ExistingModelEntry, "attachment" | "reasoning" | "temperature" | "tool_call"> {
|
||||
const result: Pick<ExistingModelEntry, "attachment" | "reasoning" | "temperature" | "tool_call"> = {};
|
||||
|
||||
// attachment: explicit user flag wins, then catalog attachment, then vision, then image modality.
|
||||
if (typeof existing?.attachment === "boolean") {
|
||||
result.attachment = existing.attachment;
|
||||
} else if (catalog?.capabilities) {
|
||||
if (typeof catalog.capabilities.attachment === "boolean") {
|
||||
result.attachment = catalog.capabilities.attachment;
|
||||
} else if (catalog.capabilities.vision === true) {
|
||||
result.attachment = true;
|
||||
} else if (
|
||||
Array.isArray(catalog.input_modalities) &&
|
||||
catalog.input_modalities.includes("image")
|
||||
) {
|
||||
result.attachment = true;
|
||||
}
|
||||
}
|
||||
|
||||
// reasoning: explicit user flag wins, then catalog reasoning.
|
||||
if (typeof existing?.reasoning === "boolean") {
|
||||
result.reasoning = existing.reasoning;
|
||||
} else if (catalog?.capabilities?.reasoning === true) {
|
||||
result.reasoning = true;
|
||||
}
|
||||
|
||||
// temperature: explicit user flag wins, then catalog temperature.
|
||||
if (typeof existing?.temperature === "boolean") {
|
||||
result.temperature = existing.temperature;
|
||||
} else if (catalog?.capabilities?.temperature === true) {
|
||||
result.temperature = true;
|
||||
}
|
||||
|
||||
// tool_call: explicit user flag wins, then catalog tool_calling.
|
||||
if (typeof existing?.tool_call === "boolean") {
|
||||
result.tool_call = existing.tool_call;
|
||||
} else if (catalog?.capabilities?.tool_calling === true) {
|
||||
result.tool_call = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveContextLength(entry: CatalogModelEntry): number | undefined {
|
||||
const candidates = [entry.context_length, entry.max_context_window_tokens];
|
||||
for (const c of candidates) {
|
||||
@@ -196,11 +257,15 @@ function buildModelEntry(
|
||||
|
||||
const entry: ExistingModelEntry = { name };
|
||||
|
||||
// Round-trip capability flags from the existing config (if any).
|
||||
for (const flag of ["attachment", "reasoning", "temperature", "tool_call"] as const) {
|
||||
const value = existing?.[flag];
|
||||
if (typeof value === "boolean") entry[flag] = value;
|
||||
}
|
||||
// Derive capability flags from the catalog, preserving explicit user overrides.
|
||||
// Explicit user booleans (including `false`) always win; catalog capabilities
|
||||
// fill in missing values so newly discovered models are not presented as
|
||||
// text-only to OpenCode clients.
|
||||
const caps = deriveOpenCodeCapabilities(catalog, existing);
|
||||
if (typeof caps.attachment === "boolean") entry.attachment = caps.attachment;
|
||||
if (typeof caps.reasoning === "boolean") entry.reasoning = caps.reasoning;
|
||||
if (typeof caps.temperature === "boolean") entry.temperature = caps.temperature;
|
||||
if (typeof caps.tool_call === "boolean") entry.tool_call = caps.tool_call;
|
||||
|
||||
// Preserve any extra top-level keys the user set (variants, headers, etc.)
|
||||
// that we don't model explicitly.
|
||||
|
||||
@@ -494,6 +494,46 @@ describe("config-generator", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("propagates vision capability from the live catalog for issue #8960", async () => {
|
||||
const modelId = "cx/gpt-5.6-sol-medium-issue-8960";
|
||||
const stub = stubFetchOnce(
|
||||
makeCatalogResponse([
|
||||
{
|
||||
id: modelId,
|
||||
owned_by: "codex",
|
||||
context_length: 272000,
|
||||
max_output_tokens: 128000,
|
||||
capabilities: {
|
||||
vision: true,
|
||||
reasoning: true,
|
||||
tool_calling: true,
|
||||
},
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
])
|
||||
);
|
||||
try {
|
||||
const { generateOpencodeConfig } = await import(
|
||||
"../../../src/lib/cli-helper/config-generator/opencode.ts"
|
||||
);
|
||||
const out = await generateOpencodeConfig({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test",
|
||||
});
|
||||
const cfg = JSON.parse(out);
|
||||
const model = cfg.provider.omniroute.models[modelId];
|
||||
|
||||
assert.strictEqual(
|
||||
model.attachment,
|
||||
true,
|
||||
"a catalog model with vision/image input must remain attachment-capable in opencode.json"
|
||||
);
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-pulls the Opencode FREE Omni combo context (the user-reported case)", async () => {
|
||||
// Regression guard: the catalog's min-of-targets for combos must be
|
||||
// reflected verbatim. No hardcoded 128K, no fallback that overrides
|
||||
|
||||
Reference in New Issue
Block a user