Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions src/providers/stale-context-window-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
*
* This rewrites one thing: a window whose saved value is still byte-for-byte the
* wrong number this file names, on a provider that still carries the registry's
* adapter. A value the user changed does not match `from` and is left alone, and
* nothing else in the row is touched. Same shape and the same restraint as
* `model-rename-migration`, for the case where the id was right and the number
* was not.
* adapter and still points at the registry's own endpoint. A value the user
* changed does not match `from` and is left alone, and nothing else in the row
* is touched. Same shape and the same restraint as `model-rename-migration`,
* for the case where the id was right and the number was not.
*/
import { PROVIDER_REGISTRY } from "./registry";
import type { OcxConfig } from "../types";
import type { OcxConfig, OcxProviderConfig } from "../types";

export interface StaleContextWindow {
/** Registry provider id whose saved rows may carry the wrong window. */
Expand All @@ -34,15 +34,18 @@ export interface StaleContextWindowProjection {
}

/**
* Cognition windows corrected against a live `GetCascadeModelConfigs` response.
* Known-bad registry context windows corrected against provider evidence.
*
* The shipped table had been assembled from each model's ORIGINAL vendor window
* The Alibaba Token Plan correction comes from gateway boundary probes. The
* shipped Cognition table had been assembled from each model's ORIGINAL vendor window
* rather than from what Cognition serves, so the Claude rows claimed 200k against
* an actual 1M and Grok claimed 256k against 500k. Cognition documents no window
* anywhere, so the per-account catalog is the only first-party source; these are
* the degraded-mode figures, and live discovery supersedes them when it runs.
*/
export const STALE_CONTEXT_WINDOWS: readonly StaleContextWindow[] = [
{ provider: "alibaba-token-plan", model: "qwen3.8-max", from: 983_616, to: 1_000_000 },
{ provider: "alibaba-token-plan-intl", model: "qwen3.8-max", from: 983_616, to: 1_000_000 },
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
{ provider: "devin", model: "swe-1-7", from: 256_000, to: 262_000 },
{ provider: "devin", model: "swe-1-7-lightning", from: 256_000, to: 202_752 },
{ provider: "devin", model: "gpt-5-6-sol", from: 1_050_000, to: 1_000_000 },
Expand All @@ -55,9 +58,22 @@ export const STALE_CONTEXT_WINDOWS: readonly StaleContextWindow[] = [
{ provider: "devin", model: "grok-4-5", from: 256_000, to: 500_000 },
];

function providerStillMatchesRegistry(id: string, adapter: unknown): boolean {
const entry = PROVIDER_REGISTRY.find(row => row.id === id);
return entry !== undefined && entry.adapter === adapter;
/**
* Only repair a row that still points at the registry's own endpoint. The
* adapter alone cannot tell the registry provider from another destination —
* a repointed `alibaba-token-plan-intl` row keeps the generic `openai-chat`
* adapter, but the windows on its custom gateway are the user's own figures.
* Same ownership rule as `model-rename-migration`.
*/
function providerStillMatchesRegistry(name: string, prov: OcxProviderConfig): boolean {
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
if (!entry || entry.adapter !== prov.adapter) return false;
if (!prov.baseUrl || !entry.baseUrl) return true;
const choices = entry.baseUrlChoices?.map(choice => choice.baseUrl) ?? [];
const known = [entry.baseUrl, ...choices]
.filter((url): url is string => typeof url === "string")
.map(url => url.replace(/\/+$/, ""));
return known.includes(prov.baseUrl.replace(/\/+$/, ""));
Comment on lines +73 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Equivalent registry URLs can prevent the context-window repair

An official endpoint with whitespace or URL-equivalent casing fails this string comparison because normalization removes only trailing slashes. Valid saved rows then retain stale context windows.

Learn more

Provider baseUrl validation parses baseUrl.trim(), so surrounding whitespace and URL casing remain valid configuration. URL schemes and hostnames are case-insensitive, but this comparison treats them as ordinary case-sensitive text after removing only trailing slashes. The repository already has URL-aware endpoint normalization in normalizedProviderEndpoint, although it is currently private.

Example: A saved Alibaba row using HTTPS://TOKEN-PLAN.CN-BEIJING.MAAS.ALIYUNCS.COM/compatible-mode/v1/ still targets the registry host. This check rejects it, so qwen3.8-max remains at 983,616 instead of becoming 1,000,000.

Recommended fix: Reuse or extract the URL-aware normalization used by normalizedProviderEndpoint, and apply it to both registry choices and the configured URL. Add focused coverage for surrounding whitespace and scheme/host casing without lowercasing case-sensitive path components.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}

/** Pure projection. The caller decides whether to persist. */
Expand All @@ -71,7 +87,7 @@ export function projectStaleContextWindows(
for (const entry of entries) {
const prov = config.providers?.[entry.provider];
if (!prov) continue;
if (!providerStillMatchesRegistry(entry.provider, prov.adapter)) continue;
if (!providerStillMatchesRegistry(entry.provider, prov)) continue;
const windows = prov.modelContextWindows;
if (!windows || windows[entry.model] !== entry.from) continue;
windows[entry.model] = entry.to;
Expand All @@ -89,4 +105,3 @@ export function projectStaleContextWindows(

return { config, changed: repaired.size > 0, warnings };
}

3 changes: 3 additions & 0 deletions structure/providers-and-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ Because `enrichProviderFromRegistry` fills `noVisionModels` all-or-nothing and f
frozen into any config saved while it was current. `src/providers/stale-vision-classification-migration.ts`
repairs exactly those two saved values and runs inside the shared startup repair pass in
`src/providers/model-rename-startup.ts`. Correcting the registry alone fixes new installs only.
The same startup pass uses `src/providers/stale-context-window-migration.ts` to replace only exact
known-bad saved context-window seeds; this includes both Alibaba Token Plan variants' former
983,616-token `qwen3.8-max` value, while any operator-selected value remains authoritative.

It covers both states that reach a running process, because the sidecar predicate reads
`noVisionModels` before `modelInputModalities`: the full stale pair (modalities still the stale
Expand Down
4 changes: 4 additions & 0 deletions tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const provider: OcxProviderConfig = {
baseUrl: "https://example.test/v1",
apiKey: "sk-test",
authMode: "key",
// The wire role folds to `system` unless a destination is recorded as accepting
// `developer`; this suite is about tool-result repair ordering, so it declares the
// destination rather than asserting the default.
foldDeveloperRoleToSystem: false,
};

interface ChatMsg {
Expand Down
49 changes: 47 additions & 2 deletions tests/providers/context-window-seed-repair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ function devinConfig(windows: Record<string, number>, adapter = "devin"): OcxCon
} as unknown as OcxConfig;
}

function alibabaConfig(provider: "alibaba-token-plan" | "alibaba-token-plan-intl", value: number): OcxConfig {
return { providers: { [provider]: { adapter: "openai-chat", modelContextWindows: { "qwen3.8-max": value } } } } as OcxConfig;
}

describe("stale context window migration", () => {
test("repairs a window the config inherited from the wrong registry seed", () => {
// `enrichProviderFromRegistry` is fill-only, so a config saved while the
Expand All @@ -34,6 +38,48 @@ describe("stale context window migration", () => {
expect(projection.config.providers!.devin!.modelContextWindows!["grok-4-5"]).toBe(300_000);
});

test.each(["alibaba-token-plan", "alibaba-token-plan-intl"] as const)(
"repairs the old qwen3.8-max seed for %s",
provider => {
const projection = projectStaleContextWindows(alibabaConfig(provider, 983_616));
expect(projection.changed).toBe(true);
expect(projection.config.providers![provider]!.modelContextWindows!["qwen3.8-max"]).toBe(1_000_000);
},
);

test.each(["alibaba-token-plan", "alibaba-token-plan-intl"] as const)(
"skips %s when the row was repointed to a custom gateway",
provider => {
// A repointed row keeps the provider id and the generic openai-chat
// adapter, so the adapter alone cannot tell Alibaba from another
// OpenAI-compatible destination — the 983,616 there may be that
// gateway's real limit rather than the stale registry seed.
const config = alibabaConfig(provider, 983_616);
config.providers![provider]!.baseUrl = "https://gateway.example/v1";
const projection = projectStaleContextWindows(config);
expect(projection.changed).toBe(false);
expect(projection.config.providers![provider]!.modelContextWindows!["qwen3.8-max"]).toBe(983_616);
},
);

test("repairs a row pointing at the registry endpoint with a trailing slash", () => {
const config = alibabaConfig("alibaba-token-plan", 983_616);
config.providers!["alibaba-token-plan"]!.baseUrl =
"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/";
const projection = projectStaleContextWindows(config);
expect(projection.changed).toBe(true);
expect(projection.config.providers!["alibaba-token-plan"]!.modelContextWindows!["qwen3.8-max"]).toBe(1_000_000);
});

test("repairs an intl row pointed at a declared baseUrlChoices endpoint", () => {
const config = alibabaConfig("alibaba-token-plan-intl", 983_616);
config.providers!["alibaba-token-plan-intl"]!.baseUrl =
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
const projection = projectStaleContextWindows(config);
expect(projection.changed).toBe(true);
expect(projection.config.providers!["alibaba-token-plan-intl"]!.modelContextWindows!["qwen3.8-max"]).toBe(1_000_000);
});

test("skips a row that no longer carries the registry adapter", () => {
// A `devin` row retargeted at another transport is not the provider these
// numbers describe, so rewriting its windows would be a guess.
Expand All @@ -52,8 +98,7 @@ describe("stale context window migration", () => {
// never performs, and an entry for another provider would silently do nothing.
for (const entry of STALE_CONTEXT_WINDOWS) {
expect(entry.from).not.toBe(entry.to);
expect(entry.provider).toBe("devin");
expect(["alibaba-token-plan", "alibaba-token-plan-intl", "devin"]).toContain(entry.provider);
}
});
});

4 changes: 4 additions & 0 deletions tests/responses/chat-inline-document-bytes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const chatProvider: OcxProviderConfig = {
adapter: "openai-chat",
baseUrl: "https://gateway.example.internal/v1",
apiKey: "k",
// The wire role folds to `system` unless a destination is recorded as accepting
// `developer`; the document test asserts the role a turn keeps, so it declares the
// destination rather than asserting the default.
foldDeveloperRoleToSystem: false,
};
const anthropicProvider = {
adapter: "anthropic",
Expand Down
Loading