mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 15:31:07 +00:00
* feat(github-copilot): resolve any model ID dynamically instead of only known ones * refactor(github-copilot): extract model resolution, add reasoning heuristic and tests * fix(github-copilot): default synthetic models to text-only input * ci: retrigger checks * copilot: mark synthetic catch-all models as vision-capable * fix(github-copilot): anchor reasoning regex, unexport internal constants, add mid-string test * fix(github-copilot): default synthetic models to text-only input * fix(github-copilot): restore image input for synthetic models with explanatory comment * fix(github-copilot): normalize registry lookup casing, add bare o3 test case * fix: preserve configured overrides for dynamic models * fix: allow dynamic GitHub Copilot models (#51325) (thanks @fuller-stack-dev) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@mariozechner/pi-ai/oauth", () => ({
|
|
getOAuthApiKey: vi.fn(),
|
|
getOAuthProviders: vi.fn(() => []),
|
|
}));
|
|
|
|
vi.mock("openclaw/plugin-sdk/provider-models", () => ({
|
|
normalizeModelCompat: (model: Record<string, unknown>) => model,
|
|
}));
|
|
|
|
import type { ProviderResolveDynamicModelContext } from "openclaw/plugin-sdk/core";
|
|
import { resolveCopilotForwardCompatModel } from "./models.js";
|
|
|
|
function createMockCtx(
|
|
modelId: string,
|
|
registryModels: Record<string, Record<string, unknown>> = {},
|
|
): ProviderResolveDynamicModelContext {
|
|
return {
|
|
modelId,
|
|
provider: "github-copilot",
|
|
config: {},
|
|
modelRegistry: {
|
|
find: (provider: string, id: string) => registryModels[`${provider}/${id}`] ?? null,
|
|
},
|
|
} as unknown as ProviderResolveDynamicModelContext;
|
|
}
|
|
|
|
describe("resolveCopilotForwardCompatModel", () => {
|
|
it("returns undefined for empty modelId", () => {
|
|
expect(resolveCopilotForwardCompatModel(createMockCtx(""))).toBeUndefined();
|
|
expect(resolveCopilotForwardCompatModel(createMockCtx(" "))).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined when model is already in registry", () => {
|
|
const ctx = createMockCtx("gpt-4o", {
|
|
"github-copilot/gpt-4o": { id: "gpt-4o", name: "gpt-4o" },
|
|
});
|
|
expect(resolveCopilotForwardCompatModel(ctx)).toBeUndefined();
|
|
});
|
|
|
|
it("clones gpt-5.2-codex template for gpt-5.3-codex", () => {
|
|
const template = {
|
|
id: "gpt-5.2-codex",
|
|
name: "gpt-5.2-codex",
|
|
provider: "github-copilot",
|
|
api: "openai-responses",
|
|
reasoning: true,
|
|
contextWindow: 200_000,
|
|
};
|
|
const ctx = createMockCtx("gpt-5.3-codex", {
|
|
"github-copilot/gpt-5.2-codex": template,
|
|
});
|
|
const result = resolveCopilotForwardCompatModel(ctx);
|
|
expect(result).toBeDefined();
|
|
expect(result!.id).toBe("gpt-5.3-codex");
|
|
expect(result!.name).toBe("gpt-5.3-codex");
|
|
expect((result as unknown as Record<string, unknown>).reasoning).toBe(true);
|
|
});
|
|
|
|
it("falls through to synthetic catch-all when codex template is missing", () => {
|
|
const ctx = createMockCtx("gpt-5.3-codex");
|
|
const result = resolveCopilotForwardCompatModel(ctx);
|
|
expect(result).toBeDefined();
|
|
expect(result!.id).toBe("gpt-5.3-codex");
|
|
});
|
|
|
|
it("creates synthetic model for arbitrary unknown model ID", () => {
|
|
const ctx = createMockCtx("gpt-5.4-mini");
|
|
const result = resolveCopilotForwardCompatModel(ctx);
|
|
expect(result).toBeDefined();
|
|
expect(result!.id).toBe("gpt-5.4-mini");
|
|
expect(result!.name).toBe("gpt-5.4-mini");
|
|
expect((result as unknown as Record<string, unknown>).api).toBe("openai-responses");
|
|
expect((result as unknown as Record<string, unknown>).input).toEqual(["text", "image"]);
|
|
});
|
|
|
|
it("infers reasoning=true for o1/o3 model IDs", () => {
|
|
for (const id of ["o1", "o3", "o3-mini", "o1-preview"]) {
|
|
const ctx = createMockCtx(id);
|
|
const result = resolveCopilotForwardCompatModel(ctx);
|
|
expect(result).toBeDefined();
|
|
expect((result as unknown as Record<string, unknown>).reasoning).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("sets reasoning=false for non-reasoning model IDs including mid-string o1/o3", () => {
|
|
for (const id of [
|
|
"gpt-5.4-mini",
|
|
"claude-sonnet-4.6",
|
|
"gpt-4o",
|
|
"audio-o1-hd",
|
|
"turbo-o3-voice",
|
|
]) {
|
|
const ctx = createMockCtx(id);
|
|
const result = resolveCopilotForwardCompatModel(ctx);
|
|
expect(result).toBeDefined();
|
|
expect((result as unknown as Record<string, unknown>).reasoning).toBe(false);
|
|
}
|
|
});
|
|
});
|