Skip to content

Commit 400bece

Browse files
committed
Merge remote-tracking branch 'origin/main' into chore/autofix-bot-issues-2026-07-31
2 parents fb18556 + 82d8b1a commit 400bece

7 files changed

Lines changed: 679 additions & 0 deletions

File tree

.github/workflows/sync-models.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@ jobs:
9999
100100
pnpm exec tsx packages/proxy/scripts/sync_models.ts sync-baseten --write
101101
102+
- name: Sync Cohere models from /v1/models
103+
env:
104+
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
105+
run: |
106+
set -euo pipefail
107+
108+
# Cohere's /v1/models is authoritative for the Cohere-hosted chat models
109+
# (LiteLLM lags it and even carries models Cohere has removed). Pricing
110+
# is overlaid from LiteLLM where available. Skip cleanly if the key is
111+
# not configured yet so the rest of the sync still runs.
112+
if [ -z "${COHERE_API_KEY:-}" ]; then
113+
echo "COHERE_API_KEY not set; skipping Cohere model sync."
114+
exit 0
115+
fi
116+
117+
pnpm exec tsx packages/proxy/scripts/sync_models.ts sync-cohere --write
118+
102119
- name: Sync OpenRouter alternates from /api/v1/models
103120
run: |
104121
set -euo pipefail

packages/proxy/schema/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,18 @@ export const AvailableEndpointTypes: { [name: string]: ModelEndpointType[] } = {
13171317
"command-r-plus-08-2024": ["cohere", "openrouter"],
13181318
"command-r-08-2024": ["cohere", "openrouter"],
13191319
"command-r7b-12-2024": ["cohere", "openrouter"],
1320+
"c4ai-aya-expanse-32b": ["cohere"],
1321+
"c4ai-aya-vision-32b": ["cohere"],
1322+
"command-a-plus-05-2026": ["cohere"],
1323+
"command-a-reasoning-08-2025": ["cohere"],
1324+
"command-a-translate-08-2025": ["cohere"],
1325+
"command-a-vision-07-2025": ["cohere"],
1326+
"command-r7b-arabic-02-2025": ["cohere"],
1327+
"north-mini-code-1-0": ["cohere"],
1328+
"tiny-aya-earth": ["cohere"],
1329+
"tiny-aya-fire": ["cohere"],
1330+
"tiny-aya-global": ["cohere"],
1331+
"tiny-aya-water": ["cohere"],
13201332
"databricks-gpt-5-5-pro": ["databricks"],
13211333
"databricks-gemini-3-5-flash": ["databricks"],
13221334
"databricks-gpt-5-3-codex": ["databricks"],

packages/proxy/schema/model_list.json

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10022,6 +10022,102 @@
1002210022
"bedrock"
1002310023
]
1002410024
},
10025+
"tiny-aya-water": {
10026+
"format": "openai",
10027+
"flavor": "chat",
10028+
"max_input_tokens": 8192,
10029+
"available_providers": [
10030+
"cohere"
10031+
]
10032+
},
10033+
"tiny-aya-global": {
10034+
"format": "openai",
10035+
"flavor": "chat",
10036+
"max_input_tokens": 8192,
10037+
"available_providers": [
10038+
"cohere"
10039+
]
10040+
},
10041+
"tiny-aya-fire": {
10042+
"format": "openai",
10043+
"flavor": "chat",
10044+
"max_input_tokens": 8192,
10045+
"available_providers": [
10046+
"cohere"
10047+
]
10048+
},
10049+
"tiny-aya-earth": {
10050+
"format": "openai",
10051+
"flavor": "chat",
10052+
"max_input_tokens": 8192,
10053+
"available_providers": [
10054+
"cohere"
10055+
]
10056+
},
10057+
"north-mini-code-1-0": {
10058+
"format": "openai",
10059+
"flavor": "chat",
10060+
"max_input_tokens": 436000,
10061+
"available_providers": [
10062+
"cohere"
10063+
]
10064+
},
10065+
"command-r7b-arabic-02-2025": {
10066+
"format": "openai",
10067+
"flavor": "chat",
10068+
"max_input_tokens": 128000,
10069+
"available_providers": [
10070+
"cohere"
10071+
]
10072+
},
10073+
"command-a-vision-07-2025": {
10074+
"format": "openai",
10075+
"flavor": "chat",
10076+
"max_input_tokens": 128000,
10077+
"available_providers": [
10078+
"cohere"
10079+
]
10080+
},
10081+
"command-a-translate-08-2025": {
10082+
"format": "openai",
10083+
"flavor": "chat",
10084+
"max_input_tokens": 8992,
10085+
"available_providers": [
10086+
"cohere"
10087+
]
10088+
},
10089+
"command-a-reasoning-08-2025": {
10090+
"format": "openai",
10091+
"flavor": "chat",
10092+
"max_input_tokens": 288768,
10093+
"available_providers": [
10094+
"cohere"
10095+
]
10096+
},
10097+
"command-a-plus-05-2026": {
10098+
"format": "openai",
10099+
"flavor": "chat",
10100+
"max_input_tokens": 436000,
10101+
"available_providers": [
10102+
"cohere"
10103+
]
10104+
},
10105+
"c4ai-aya-vision-32b": {
10106+
"format": "openai",
10107+
"flavor": "chat",
10108+
"max_input_tokens": 16384,
10109+
"available_providers": [
10110+
"cohere"
10111+
]
10112+
},
10113+
"c4ai-aya-expanse-32b": {
10114+
"format": "openai",
10115+
"flavor": "chat",
10116+
"max_input_tokens": 128000,
10117+
"available_providers": [
10118+
"cohere"
10119+
]
10120+
},
1002510121
"command-a-03-2025": {
1002610122
"format": "openai",
1002710123
"flavor": "chat",

packages/proxy/scripts/model_deprecation.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ describe("openrouter deprecation adapter", () => {
3333
});
3434
});
3535

36+
describe("cohere deprecation adapter", () => {
37+
it("is registered as an authoritative, list-only, non-report-only provider", () => {
38+
const api = PROVIDER_APIS.cohere;
39+
expect(api).toBeDefined();
40+
// Absence from Cohere's /v1/models alone marks a model deprecated (Cohere
41+
// hosts only its own models), so the list is authoritative and there is no
42+
// probe. This is what prunes models Cohere removes but LiteLLM still lists.
43+
expect(api.listIsAuthoritative).toBe(true);
44+
expect(api.probeModel).toBeNull();
45+
expect(api.listModels).not.toBeNull();
46+
// Must NOT be report-only, or deprecations would never be applied.
47+
expect(REPORT_ONLY_PROVIDERS.has("cohere")).toBe(false);
48+
});
49+
});
50+
3651
describe("classifyProbe", () => {
3752
it("treats 2xx as active", () => {
3853
expect(classifyProbe(200, "{}")).toBe("active");

packages/proxy/scripts/model_probe.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ const listSchema = z
9797
})
9898
.passthrough();
9999

100+
// Cohere's /v1/models is paginated: { models: [{ name }], next_page_token }.
101+
const cohereListSchema = z
102+
.object({
103+
models: z.array(z.object({ name: z.string() }).passthrough()),
104+
next_page_token: z.string().nullish(),
105+
})
106+
.passthrough();
107+
100108
// OpenAI-compatible /models responses come back either as { data: [{id}] } or,
101109
// for some providers (e.g. Together), as a bare array of { id }.
102110
function extractOpenAiModelIds(body: string): Set<string> {
@@ -300,6 +308,46 @@ export const PROVIDER_APIS: Record<string, ProviderApi> = {
300308
return new Set((parsed.endpoints ?? []).map((e) => e.name));
301309
},
302310
},
311+
// Cohere hosts only its own models and its paginated /v1/models is the
312+
// authoritative directory of what it currently serves. A catalog model with
313+
// the cohere provider that is no longer listed is deprecated (list-only, no
314+
// probe) — this is what catches models Cohere removes (e.g. `command-r`,
315+
// removed 2025-09-15) that LiteLLM still lists. We list ALL models (no endpoint
316+
// filter) so a model still served on any endpoint is not falsely deprecated;
317+
// Cohere ids are bare names, matching catalog keys directly.
318+
cohere: {
319+
listIsAuthoritative: true,
320+
probeModel: null,
321+
listModels: async (secret) => {
322+
const names = new Set<string>();
323+
let pageToken: string | undefined;
324+
// Bound the pagination so a malformed next_page_token cannot loop forever.
325+
for (let page = 0; page < 100; page++) {
326+
const url = new URL("https://api.cohere.com/v1/models");
327+
url.searchParams.set("page_size", "1000");
328+
if (pageToken) {
329+
url.searchParams.set("page_token", pageToken);
330+
}
331+
const { status, body } = await request("GET", url.toString(), {
332+
authorization: `Bearer ${secret.secret}`,
333+
});
334+
if (status >= 400) {
335+
throw new Error(
336+
`list cohere /v1/models -> HTTP ${status}: ${body.slice(0, 160)}`,
337+
);
338+
}
339+
const parsed = cohereListSchema.parse(JSON.parse(body));
340+
for (const model of parsed.models) {
341+
names.add(model.name);
342+
}
343+
if (!parsed.next_page_token) {
344+
return names;
345+
}
346+
pageToken = parsed.next_page_token;
347+
}
348+
return names;
349+
},
350+
},
303351
// OpenRouter's /api/v1/models is a public, authoritative directory of what it
304352
// serves. The audit uses it to proactively prune openrouter: a catalog model
305353
// with the openrouter provider that is no longer listed loses openrouter (and

packages/proxy/scripts/sync_models.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
applyEquivalentModels,
77
canonicalizeLocalModelsContent,
88
convertBasetenToLocalModel,
9+
convertCohereToLocalModel,
10+
applyCohereLiteLLMPricing,
11+
isSupportedCohereChatModel,
912
convertRemoteToLocalModel,
1013
applyBasetenPricing,
1114
applyOpenRouterPricing,
@@ -953,3 +956,96 @@ describe("applyOpenRouterPricing", () => {
953956
expect(applyOpenRouterPricing("x-ai/grok-4.5", model, orModel)).toBeNull();
954957
});
955958
});
959+
960+
describe("convertCohereToLocalModel", () => {
961+
it("maps a Cohere chat model to an openai-format cohere entry (no LiteLLM = no pricing)", () => {
962+
const spec = convertCohereToLocalModel({
963+
name: "command-a-reasoning-08-2025",
964+
endpoints: ["chat"],
965+
context_length: 288768,
966+
});
967+
expect(spec.format).toBe("openai");
968+
expect(spec.flavor).toBe("chat");
969+
expect(spec.max_input_tokens).toBe(288768);
970+
expect(spec.available_providers).toEqual(["cohere"]);
971+
// No LiteLLM overlay -> no fabricated pricing.
972+
expect(spec.input_cost_per_mil_tokens).toBeUndefined();
973+
expect(spec.output_cost_per_mil_tokens).toBeUndefined();
974+
});
975+
976+
it("overlays LiteLLM pricing (per-token -> per-mil) when available", () => {
977+
const spec = convertCohereToLocalModel(
978+
{
979+
name: "command-a-03-2025",
980+
endpoints: ["chat"],
981+
context_length: 256000,
982+
},
983+
{
984+
input_cost_per_token: 2.5e-6,
985+
output_cost_per_token: 1e-5,
986+
cache_read_input_token_cost: 1.25e-6,
987+
},
988+
);
989+
expect(spec.input_cost_per_mil_tokens).toBe(2.5);
990+
expect(spec.output_cost_per_mil_tokens).toBe(10);
991+
expect(spec.input_cache_read_cost_per_mil_tokens).toBe(1.25);
992+
});
993+
});
994+
995+
describe("isSupportedCohereChatModel", () => {
996+
it("accepts a live chat model", () => {
997+
expect(
998+
isSupportedCohereChatModel({
999+
name: "command-a-03-2025",
1000+
endpoints: ["chat"],
1001+
}),
1002+
).toBe(true);
1003+
});
1004+
1005+
it("rejects non-chat, fine-tuned, and deprecated models", () => {
1006+
expect(
1007+
isSupportedCohereChatModel({ name: "embed-v4.0", endpoints: ["embed"] }),
1008+
).toBe(false);
1009+
expect(
1010+
isSupportedCohereChatModel({
1011+
name: "my-finetune",
1012+
endpoints: ["chat"],
1013+
finetuned: true,
1014+
}),
1015+
).toBe(false);
1016+
expect(
1017+
isSupportedCohereChatModel({
1018+
name: "command-r",
1019+
endpoints: ["chat"],
1020+
is_deprecated: true,
1021+
}),
1022+
).toBe(false);
1023+
});
1024+
});
1025+
1026+
describe("applyCohereLiteLLMPricing", () => {
1027+
const priceless: ModelSpec = {
1028+
format: "openai",
1029+
flavor: "chat",
1030+
available_providers: ["cohere"],
1031+
};
1032+
1033+
it("fills pricing on an existing price-less entry when LiteLLM has it", () => {
1034+
const priced = applyCohereLiteLLMPricing("command-a-03-2025", priceless, {
1035+
input_cost_per_token: 2.5e-6,
1036+
output_cost_per_token: 1e-5,
1037+
cache_read_input_token_cost: 1.25e-6,
1038+
});
1039+
expect(priced?.input_cost_per_mil_tokens).toBe(2.5);
1040+
expect(priced?.output_cost_per_mil_tokens).toBe(10);
1041+
expect(priced?.input_cache_read_cost_per_mil_tokens).toBe(1.25);
1042+
});
1043+
1044+
it("returns null when LiteLLM has no entry or pricing is unchanged", () => {
1045+
expect(applyCohereLiteLLMPricing("x", priceless, undefined)).toBeNull();
1046+
const already: ModelSpec = { ...priceless, input_cost_per_mil_tokens: 2.5 };
1047+
expect(
1048+
applyCohereLiteLLMPricing("x", already, { input_cost_per_token: 2.5e-6 }),
1049+
).toBeNull();
1050+
});
1051+
});

0 commit comments

Comments
 (0)