mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
fix(github): honor per-model targetFormat override for Copilot custom models (#8713)
GithubExecutor.buildUrl() only consulted the static PROVIDER_MODELS registry
via getModelTargetFormat("gh", model), so a custom Copilot model (e.g.
gpt-5.6-terra/gpt-5.6-luna) with its dashboard "Target Format" set to
OpenAI Responses API always still routed to /chat/completions and got
rejected upstream with "model ... is not accessible via the
/chat/completions endpoint" — the setting had no effect on real routing.
chatCore already resolves the correct per-request targetFormat (including
the custom-model override) via resolveChatCoreTargetFormat(), but that value
was never threaded past chatCore into the executor's own URL-building
decision. Mirrors the zai/glm-coding-apikey fix (#7364) for the identical
class of bug: chatCore/executionCredentials.ts now surfaces the resolved
override onto providerSpecificData.targetFormat when it resolves to
openai-responses for the github provider, and GithubExecutor.buildUrl()
prefers that value over the static registry lookup when present.
Verified: 6 new regression tests plus all 95 pre-existing github/executor
tests green.
Co-authored-by: Wital <wital@example.com>
This commit is contained in:
@@ -59,8 +59,24 @@ export class GithubExecutor extends BaseExecutor {
|
||||
return !(m.includes("gemini") || m.includes("claude"));
|
||||
}
|
||||
|
||||
buildUrl(model: string, _stream: boolean, _urlIndex = 0) {
|
||||
const targetFormat = getModelTargetFormat("gh", model);
|
||||
buildUrl(
|
||||
model: string,
|
||||
_stream: boolean,
|
||||
_urlIndex = 0,
|
||||
credentials?: ProviderCredentials | null
|
||||
) {
|
||||
// #2905/#7364-pattern: a custom Copilot model's per-model targetFormat
|
||||
// override isn't in the static PROVIDER_MODELS registry, so
|
||||
// getModelTargetFormat() can't see it. chatCore/executionCredentials.ts
|
||||
// threads the resolved override onto providerSpecificData.targetFormat
|
||||
// for exactly this case — prefer it when present.
|
||||
const overrideTargetFormat = (
|
||||
credentials as { providerSpecificData?: { targetFormat?: unknown } }
|
||||
)?.providerSpecificData?.targetFormat;
|
||||
const targetFormat =
|
||||
typeof overrideTargetFormat === "string"
|
||||
? overrideTargetFormat
|
||||
: getModelTargetFormat("gh", model);
|
||||
// Claude models: route to Copilot's Anthropic-native /v1/messages shim — the
|
||||
// only Copilot endpoint that surfaces prompt-cache token counts for Claude and
|
||||
// avoids a lossy round-trip of tool_use/tool_result/thinking content blocks
|
||||
|
||||
@@ -150,8 +150,18 @@ export function resolveExecutionCredentials(opts: {
|
||||
providerSpecificData.targetFormat = targetFormat;
|
||||
}
|
||||
|
||||
applyKimiExecutionMetadata(providerSpecificData, provider, targetFormat, modelInfo);
|
||||
// GitHub Copilot custom models (custom-model dropdown, #2905) can carry a
|
||||
// per-model targetFormat override resolving to "openai-responses" so a
|
||||
// Codex-family custom model routes through Copilot's native /responses
|
||||
// endpoint. GithubExecutor.buildUrl() only consults the static
|
||||
// PROVIDER_MODELS registry via getModelTargetFormat() and has no other way
|
||||
// to see a custom model's override — mirrors the zai/glm-coding-apikey fix
|
||||
// (#7364) for the same class of bug.
|
||||
if (targetFormat === FORMATS.OPENAI_RESPONSES && provider === "github") {
|
||||
providerSpecificData.targetFormat = targetFormat;
|
||||
}
|
||||
|
||||
applyKimiExecutionMetadata(providerSpecificData, provider, targetFormat, modelInfo);
|
||||
const withApiType = {
|
||||
...nextCredentials,
|
||||
providerSpecificData,
|
||||
|
||||
86
tests/unit/github-copilot-custom-model-target-format.test.ts
Normal file
86
tests/unit/github-copilot-custom-model-target-format.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// tests/unit/github-copilot-custom-model-target-format.test.ts
|
||||
// GitHub Copilot custom models (custom-model dropdown, #2905) can carry a
|
||||
// per-model targetFormat override resolving to "openai-responses" — e.g. a
|
||||
// Codex-family custom model (gpt-5.6-terra/gpt-5.6-luna) that the operator
|
||||
// wants routed through Copilot's native /responses endpoint instead of
|
||||
// /chat/completions. GithubExecutor.buildUrl() only reads the static
|
||||
// PROVIDER_MODELS registry via getModelTargetFormat("gh", model) and has no
|
||||
// other way to see a custom model's override, so every custom Copilot model
|
||||
// silently hit /chat/completions regardless of the dashboard's Target Format
|
||||
// setting and got rejected upstream with "not accessible via the
|
||||
// /chat/completions endpoint". Mirrors the zai/glm-coding-apikey fix (#7364)
|
||||
// for the same class of bug.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { GithubExecutor } from "../../open-sse/executors/github.ts";
|
||||
import { resolveExecutionCredentials } from "../../open-sse/handlers/chatCore/executionCredentials.ts";
|
||||
|
||||
test("BUG: GithubExecutor.buildUrl ignores a per-model targetFormat:'openai-responses' override and still returns the chat/completions URL", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const credentialsWithoutOverride = { apiKey: "test-token" };
|
||||
const url = executor.buildUrl("gpt-5.6-terra", false, 0, credentialsWithoutOverride);
|
||||
assert.ok(
|
||||
!url.endsWith("/responses"),
|
||||
"sanity check: with no override and a non-codex custom model id, buildUrl falls back to chat/completions"
|
||||
);
|
||||
});
|
||||
|
||||
test("FIX: GithubExecutor.buildUrl honors providerSpecificData.targetFormat:'openai-responses' for a custom model", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const credentialsWithOverride = {
|
||||
apiKey: "test-token",
|
||||
providerSpecificData: { targetFormat: "openai-responses" },
|
||||
};
|
||||
const url = executor.buildUrl("gpt-5.6-terra", false, 0, credentialsWithOverride);
|
||||
assert.ok(
|
||||
url.endsWith("/responses"),
|
||||
`expected the /responses endpoint when the override is set, got: ${url}`
|
||||
);
|
||||
});
|
||||
|
||||
test("FIX: a Gemini/Claude custom model is never routed to /responses even with the override set (supportsResponsesEndpoint gate)", () => {
|
||||
const executor = new GithubExecutor();
|
||||
const credentialsWithOverride = {
|
||||
apiKey: "test-token",
|
||||
providerSpecificData: { targetFormat: "openai-responses" },
|
||||
};
|
||||
const url = executor.buildUrl("gemini-2.5-pro", false, 0, credentialsWithOverride);
|
||||
assert.ok(
|
||||
!url.endsWith("/responses"),
|
||||
"9router#1536 invariant: Gemini/Claude models must never route to /responses, even with a targetFormat override"
|
||||
);
|
||||
});
|
||||
|
||||
const base = {
|
||||
credentials: { providerSpecificData: { foo: "bar" } } as Record<string, unknown>,
|
||||
nativeCodexPassthrough: false,
|
||||
endpointPath: "/v1/messages",
|
||||
ccSessionId: null,
|
||||
};
|
||||
|
||||
test("github + resolved openai-responses targetFormat threads providerSpecificData.targetFormat", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "github",
|
||||
targetFormat: "openai-responses",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar", targetFormat: "openai-responses" });
|
||||
});
|
||||
|
||||
test("github + default (non-responses) targetFormat does NOT inject a targetFormat override", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "github",
|
||||
targetFormat: "openai",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar" });
|
||||
});
|
||||
|
||||
test("unrelated provider (openai) with targetFormat=openai-responses is untouched by the github branch", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "openai",
|
||||
targetFormat: "openai-responses",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar" });
|
||||
});
|
||||
Reference in New Issue
Block a user