fix(copilot): keep Gemini working without live discovery (#131143)

Declare the Gemini Chat Completions transport and request compatibility in the owning model manifest. Remove the private metadata decoration loop so static catalog selection and forward-compatible synthesis use the same declared contract.
This commit is contained in:
Peter Steinberger
2026-08-27 12:58:38 -07:00
committed by GitHub
parent 98ba35ab6f
commit 937a31c4a4
4 changed files with 122 additions and 6 deletions
+3 -2
View File
@@ -289,8 +289,9 @@ configured default model is never replaced.
<Accordion title="Transport selection">
Claude model IDs use the Anthropic Messages transport automatically.
Gemini models use the OpenAI Chat Completions transport; GPT and o-series
models keep the OpenAI Responses transport. OpenClaw selects the correct
transport based on the model ref.
models keep the OpenAI Responses transport. The bundled static catalog
includes these transports and request compatibility settings, so Gemini
keeps using Chat Completions when live discovery is disabled or unavailable.
</Accordion>
<Accordion title="Thinking levels">
@@ -26,10 +26,6 @@ const manifestModels = buildManifestModelProviderConfig({
providerId: "github-copilot",
catalog: manifestCatalog,
}).models;
for (const model of manifestModels) {
model.api = resolveCopilotTransportApi(model.id);
model.compat = { ...resolveCopilotModelCompat(model.id), ...model.compat };
}
const STATIC_MODEL_OVERRIDES = new Map<string, Partial<ModelDefinitionConfig>>([
...manifestModels.map((model) => [model.id, model] as const),
@@ -97,6 +97,13 @@
{
"id": "gemini-3.6-flash",
"name": "Gemini 3.6 Flash",
"api": "openai-completions",
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false,
"supportsUsageInStreaming": false,
"maxTokensField": "max_tokens"
},
"input": ["text", "image"],
"contextWindow": 1048576,
"maxTokens": 65536,
@@ -105,6 +112,13 @@
{
"id": "gemini-3.1-pro-preview",
"name": "Gemini 3.1 Pro Preview",
"api": "openai-completions",
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false,
"supportsUsageInStreaming": false,
"maxTokensField": "max_tokens"
},
"input": ["text", "image"],
"contextWindow": 1048576,
"maxTokens": 65536,
@@ -113,6 +127,13 @@
{
"id": "gemini-3.5-flash",
"name": "Gemini 3.5 Flash",
"api": "openai-completions",
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false,
"supportsUsageInStreaming": false,
"maxTokensField": "max_tokens"
},
"status": "deprecated",
"replacedBy": "gemini-3.6-flash",
"input": ["text", "image"],
@@ -123,6 +144,13 @@
{
"id": "gemini-2.5-pro",
"name": "Gemini 2.5 Pro",
"api": "openai-completions",
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false,
"supportsUsageInStreaming": false,
"maxTokensField": "max_tokens"
},
"status": "deprecated",
"replacedBy": "gemini-3.1-pro-preview",
"input": ["text", "image"],
@@ -1,5 +1,7 @@
// Provider runtime contract helpers define reusable runtime tests for provider plugins.
import { normalizeModelCatalog } from "@openclaw/model-catalog-core/model-catalog-normalize";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createPluginMetadataSnapshot } from "../../config/plugin-auto-enable.test-helpers.js";
import type { ProviderRuntimeModel } from "../plugin-entry.js";
import { registerProviderPlugin, requireRegisteredProvider } from "../plugin-test-runtime.js";
import { buildManifestModelProviderConfig } from "../provider-catalog-shared.js";
@@ -283,6 +285,95 @@ export function describeGithubCopilotProviderRuntimeContract(
]);
const createManifestModel = createManifestModelFactory("github-copilot", manifestCatalog);
it.each([
["gemini-3.6-flash", "openai-completions", false],
["gemini-3.1-pro-preview", "openai-completions", false],
["gemini-3.5-flash", "openai-completions", false],
["gemini-2.5-pro", "openai-completions", false],
["gpt-5.6-sol", "openai-responses", false],
["claude-sonnet-5", "anthropic-messages", false],
["gemini-3.6-flash", "openai-completions", true],
] as const)(
"routes static %s through %s with discovery enabled=%s",
async (modelId, api, discoveryEnabled) => {
const { createBundledStaticCatalogModelResolver } =
await import("../../agents/embedded-agent-runner/model.static-catalog.js");
const config = {
models: { catalogRefresh: { enabled: false } },
plugins: {
entries: {
"github-copilot": { config: { discovery: { enabled: discoveryEnabled } } },
},
},
};
const metadataSnapshot = createPluginMetadataSnapshot({
config,
manifestRegistry: {
diagnostics: [],
plugins: [
{
id: "github-copilot",
origin: "bundled",
providers: ["github-copilot"],
channels: [],
cliBackends: [],
skills: [],
hooks: [],
rootDir: "/fixtures/github-copilot",
source: "/fixtures/github-copilot/index.js",
manifestPath: "/fixtures/github-copilot/openclaw.plugin.json",
modelCatalog: normalizeModelCatalog(
{
providers: { "github-copilot": manifestCatalog },
discovery: { "github-copilot": "runtime" },
},
{ ownedProviders: new Set(["github-copilot"]) },
),
},
],
},
});
const resolveModel = createBundledStaticCatalogModelResolver({
cfg: config,
env: {},
metadataSnapshot,
includeRuntimeDiscovery: true,
});
const model = resolveModel({ provider: "github-copilot", modelId });
expect(model?.api).toBe(api);
if (api === "openai-completions") {
expect(model?.compat).toMatchObject({
supportsStore: false,
supportsDeveloperRole: false,
supportsUsageInStreaming: false,
maxTokensField: "max_tokens",
});
}
const provider = requireProviderContractProvider("github-copilot");
expect(
provider.preferRuntimeResolvedModel?.({
config,
provider: "github-copilot",
modelId,
}),
).toBe(discoveryEnabled);
// A missing prepared live row also occurs when enabled discovery is unavailable.
expect(
provider.resolveDynamicModel?.({
config,
provider: "github-copilot",
modelId,
modelRegistry: {
find: () => model,
getAll: () => (model ? [model] : []),
getAvailable: () => (model ? [model] : []),
hasConfiguredAuth: () => false,
},
}),
).toBeUndefined();
},
);
it("owns Copilot-specific forward-compat fallbacks", () => {
const provider = requireProviderContractProvider("github-copilot");
const expected = createManifestModel("gpt-5.4");