diff --git a/src/adapters/devin/live-models.ts b/src/adapters/devin/live-models.ts index a34909cc97a..e95419256fc 100644 --- a/src/adapters/devin/live-models.ts +++ b/src/adapters/devin/live-models.ts @@ -58,7 +58,11 @@ export const DEVIN_STATIC_MODELS = [ * number over from the model's original vendor. */ export const DEVIN_MODEL_CONTEXT_WINDOWS: Record = { - "swe-2": 262_000, + // 261112: live catalog context_window 850_000; a real turn completed at 754,971 total. + "swe-2": 850_000, + // 261112: live catalog also advertises 850_000, but observed turns terminate at + // totalTokens 262,140 -- the serving bound is ~256KiB, not the catalog figure. + "swe-2-flash": 262_000, "swe-1-7": 262_000, "swe-1-7-lightning": 202_752, "swe-1-6": 200_000, diff --git a/src/providers/stale-context-window-migration.ts b/src/providers/stale-context-window-migration.ts index 5fb1eea2ecc..ab9e3de5b87 100644 --- a/src/providers/stale-context-window-migration.ts +++ b/src/providers/stale-context-window-migration.ts @@ -43,6 +43,8 @@ export interface StaleContextWindowProjection { * the degraded-mode figures, and live discovery supersedes them when it runs. */ export const STALE_CONTEXT_WINDOWS: readonly StaleContextWindow[] = [ + // swe-2 shipped 262_000 (a stale vendor figure) before the measured 850_000 window. + { provider: "devin", model: "swe-2", from: 262_000, to: 850_000 }, { 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 }, diff --git a/src/server/responses/input-admission.ts b/src/server/responses/input-admission.ts index c8e488abe46..627daa1663c 100644 --- a/src/server/responses/input-admission.ts +++ b/src/server/responses/input-admission.ts @@ -11,6 +11,7 @@ import { clientFacingModelText, type ClientFacingModelLabel } from "../../lib/er * catches the pathological case and stays out of the way otherwise. Every uncertainty * resolves toward admitting. */ +import { collapseDevinModelUid, DEVIN_MODEL_CONTEXT_WINDOWS } from "../../adapters/devin/live-models"; import { nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, @@ -171,6 +172,28 @@ interface ResolvedContextLimits { ceiling: number | null; } +/** + * Adapter-owned static window, consulted only when the routed provider config declares none. + * Devin is the case that matters: its live catalog windows reach the Codex-facing model list + * but never `provider.modelContextWindows`, so without this fallback every devin target + * resolved `window: null` and the strict combo gate stayed fail-open on exactly the turns it + * exists to skip. Explicit provider config still wins; the effort-suffix collapse covers + * catalog UIDs like `swe-2-flash-medium`. + */ +function adapterContextWindow(provider: OcxProviderConfig, modelId: string): number | null { + if (provider.adapter !== "devin") return null; + // Devin spells the same model dotted in catalog families (`swe-1.7`) and hyphenated + // in uids (`swe-1-7`); normalize before either lookup so both spellings resolve. + const normalized = modelId.replace(/\./g, "-"); + const collapsed = collapseDevinModelUid(normalized); + // A configured window for the collapsed base (operator override or registry seed) still + // wins over the static table for effort-suffixed ids like swe-2-flash-medium. + return positive(modelRecordValue(provider.modelContextWindows, normalized)) + ?? positive(modelRecordValue(provider.modelContextWindows, collapsed)) + ?? positive(modelRecordValue(DEVIN_MODEL_CONTEXT_WINDOWS, normalized)) + ?? positive(modelRecordValue(DEVIN_MODEL_CONTEXT_WINDOWS, collapsed)); +} + function resolveContextLimits( provider: OcxProviderConfig, providerName: string, @@ -202,9 +225,12 @@ function resolveContextLimits( : null; const nativeMaxInput = canonicalNativeBare ? positive(nativeOpenAiMaxInputTokens(modelId, nativeLimits)) : null; - const window = canonicalNativeBare ? (native ?? generatedNativeWindow(modelId, configured, nativeContextCap)) : configured; + const window = canonicalNativeBare ? (native ?? generatedNativeWindow(modelId, configured, nativeContextCap)) : (configured ?? adapterContextWindow(provider, modelId)); // modelMaxInputTokens is an input-only cap, so it can only tighten the window. - const configuredMaxInput = positive(modelRecordValue(provider.modelMaxInputTokens, modelId)); + const configuredMaxInput = positive(modelRecordValue(provider.modelMaxInputTokens, modelId)) + ?? (provider.adapter === "devin" + ? positive(modelRecordValue(provider.modelMaxInputTokens, modelId.replace(/\./g, "-"))) + : null); const limits = [window, configuredMaxInput, nativeMaxInput].filter((v): v is number => v !== null); return { window, ceiling: limits.length === 0 ? null : Math.min(...limits) }; } diff --git a/structure/transports/responses-failover.md b/structure/transports/responses-failover.md index b4463758cfd..3e3c41a4f64 100644 --- a/structure/transports/responses-failover.md +++ b/structure/transports/responses-failover.md @@ -498,11 +498,19 @@ error naming a limit the model never approached — and by then output has commi target may be tried. When every failed target of an exhausted combo was refused this way with a send count of 0, `src/server/responses/core-combo.ts` keeps the `context_length_exceeded` overflow envelope that clients compact on, but replaces the provider wording from `context-overflow.ts` with a proxy-owned message saying the turn was refused locally, carrying each tried target's estimate and window (a target skipped for cooldown is not claimed to be too small). Those attempts, and the final row, keep the code `input_admission_refused` instead of `http_413`. One target that reached a provider keeps the provider wording for the whole combo. Scope is deliberately narrow. Direct and single-target requests keep the loose 2.5x -pathological-input gate, because they have nowhere to hop. Compaction turns stay exempt. Unknown -context and a caller that declared no output allowance both remain fail-open, so this invents no -limits for custom providers. Canonical native slugs that the narrower pinned table does not carry -resolve their window from the generated in-tree bundle, which is what made the gate inert on the -route where this was first observed; explicit provider and operator caps may only narrow it. +pathological-input gate, because they have nowhere to hop. Compaction turns stay exempt. A +combo request that declared no output allowance cannot price headroom, so it falls back to +the same loose 2.5x pathological-input gate direct requests use rather than the strict +input-plus-headroom check; only a request whose ceiling never resolves stays fail-open, so +this invents no limits for custom providers. When the routed provider config declares no window at all, the devin +adapter falls back to its own static table (`DEVIN_MODEL_CONTEXT_WINDOWS` in +`src/adapters/devin/live-models.ts`, effort suffixes collapsed), because live catalog windows +reach the model list but never `provider.modelContextWindows` — without it every devin combo +target resolved `window: null` and the strict gate admitted turns that could only end in a +committed-output `max_output_tokens` stop. Canonical native slugs that the narrower pinned +table does not carry resolve their window from the generated in-tree bundle, which is what +made the gate inert on the route where this was first observed; explicit provider and +operator caps may only narrow it. Regression coverage: `tests/server/input-admission.test.ts`, `tests/helpers/combo-context-headroom-cases.ts` and `tests/responses/responses-context-overflow.test.ts`. diff --git a/tests/providers/context-window-seed-repair.test.ts b/tests/providers/context-window-seed-repair.test.ts index 55063afa4e8..24d56cbbd0b 100644 --- a/tests/providers/context-window-seed-repair.test.ts +++ b/tests/providers/context-window-seed-repair.test.ts @@ -34,6 +34,20 @@ describe("stale context window migration", () => { expect(projection.config.providers!.devin!.modelContextWindows!["grok-4-5"]).toBe(300_000); }); + test("repairs the swe-2 window seeded while the table read 262_000", () => { + const config = devinConfig({ "swe-2": 262_000 }); + const projection = projectStaleContextWindows(config); + expect(projection.changed).toBe(true); + expect(projection.config.providers!.devin!.modelContextWindows!["swe-2"]).toBe(850_000); + }); + + test("leaves a deliberate swe-2 override alone", () => { + const config = devinConfig({ "swe-2": 300_000 }); + const projection = projectStaleContextWindows(config); + expect(projection.changed).toBe(false); + expect(projection.config.providers!.devin!.modelContextWindows!["swe-2"]).toBe(300_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. diff --git a/tests/providers/devin-adapter.test.ts b/tests/providers/devin-adapter.test.ts index 823c0b0a1a2..4ae80a2f3bb 100644 --- a/tests/providers/devin-adapter.test.ts +++ b/tests/providers/devin-adapter.test.ts @@ -449,7 +449,7 @@ describe("devin adapter", () => { expect(DEVIN_MODEL_CONTEXT_WINDOWS["claude-haiku-5-5"]).toBe(1_000_000); expect(DEVIN_MODEL_CONTEXT_WINDOWS["grok-4-5"]).toBe(500_000); expect(DEVIN_MODEL_CONTEXT_WINDOWS["gpt-5-6-sol"]).toBe(1_000_000); - expect(DEVIN_MODEL_CONTEXT_WINDOWS["swe-2"]).toBe(262_000); + expect(DEVIN_MODEL_CONTEXT_WINDOWS["swe-2"]).toBe(850_000); // Every statically advertised model needs one, or the picker reports 128k. for (const model of DEVIN_STATIC_MODELS) { expect(DEVIN_MODEL_CONTEXT_WINDOWS[model]).toBeGreaterThan(0); diff --git a/tests/server/input-admission.test.ts b/tests/server/input-admission.test.ts index c9c196a0c5b..0f39ca66890 100644 --- a/tests/server/input-admission.test.ts +++ b/tests/server/input-admission.test.ts @@ -396,23 +396,62 @@ describe("combo target input admission", () => { }); }); -for (const headroom of [false, true]) test(`input ceiling messages retain the actual context window headroom=${headroom}`, async () => { - const { inputAdmissionRefusalMessage } = await import("../../src/server/responses/input-admission"); - const { clientFacingModelLabel } = await import("../../src/lib/error-destination"); - const parsed = request([userText("a".repeat(4000))]); - if (headroom) parsed.options.maxOutputTokens = 8; - const provider: OcxProviderConfig = { adapter: "openai-chat", baseUrl: "https://provider.example/v1", contextWindow: 200, modelMaxInputTokens: { "test-model": 16 } }; - const result = headroom ? checkComboTargetInputAdmission(parsed, provider, "fixture", "test-model") : checkInputAdmission(parsed, provider, "fixture", "test-model"); - expect(result.admitted).toBe(false); expect(result.ceiling).toBe(16); expect(result.contextWindow).toBe(200); - const message = inputAdmissionRefusalMessage(result, clientFacingModelLabel("client-model")); - expect(message).toContain("input limit of client-model (16 tokens; context window: 200 tokens)"); -}); +describe("devin adapter window fallback", () => { + const devin: OcxProviderConfig = { adapter: "devin", baseUrl: "https://server.codeium.com" }; + const devinRequest = (inputTokens: number, modelId: string): OcxParsedRequest => ({ + ...request([userText(asciiTokens(inputTokens))]), + modelId, + options: { maxOutputTokens: 64_000 }, + }); -test("output headroom compares against the shared window after the input cap", async () => { - const { inputAdmissionRefusalMessage } = await import("../../src/server/responses/input-admission"); - const { clientFacingModelLabel } = await import("../../src/lib/error-destination"); - const parsed = request([userText("hi")]); parsed.options.maxOutputTokens = 200; - const result = checkComboTargetInputAdmission(parsed, { adapter: "openai-chat", baseUrl: "https://provider.example/v1", contextWindow: 100, modelMaxInputTokens: { "test-model": 16 } }, "fixture", "test-model"); - expect(result.admitted).toBe(false); expect(result.estimatedTokens).toBeLessThanOrEqual(16); - expect(inputAdmissionRefusalMessage(result, clientFacingModelLabel("client-model"))).toContain("context window of client-model (100 tokens; input limit: 16 tokens)"); + test("resolves the adapter window when provider config declares none", () => { + // swe-2-flash serves ~256KiB even though the live catalog advertises 850k; + // observed turns terminate at totalTokens 262,140. + expect(resolveInputCeiling(devin, "devin", "swe-2-flash")).toBe(262_000); + // Effort-suffixed catalog UIDs collapse to the base row. + expect(resolveInputCeiling(devin, "devin", "swe-2-flash-medium")).toBe(262_000); + expect(resolveInputCeiling(devin, "devin", "swe-2")).toBe(850_000); + }); + + test("a turn that cannot fit is skipped before any upstream bytes", () => { + // 250k estimated input + 64k declared output exceeds the 262k serving window -- + // the shape that used to answer 200, emit a few hundred tokens and stop on + // max_output_tokens with output already committed and no target left to try. + const result = checkComboTargetInputAdmission(devinRequest(250_000, "swe-2-flash"), devin, "devin", "swe-2-flash"); + expect(result.admitted).toBe(false); + expect(result.ceiling).toBe(262_000); + }); + + test("explicit provider windows still win over the adapter table", () => { + const wider: OcxProviderConfig = { ...devin, contextWindow: 900_000 }; + expect(checkComboTargetInputAdmission(devinRequest(250_000, "swe-2-flash"), wider, "devin", "swe-2-flash").admitted).toBe(true); + const narrower: OcxProviderConfig = { ...devin, modelContextWindows: { "swe-2-flash": 200_000 } }; + expect(resolveInputCeiling(narrower, "devin", "swe-2-flash")).toBe(200_000); + }); + + test("dotted devin ids resolve the hyphenated window", () => { + const provider: OcxProviderConfig = { adapter: "devin", baseUrl: "https://devin.test" }; + expect(resolveInputCeiling(provider, "devin", "swe-1.7")).toBe(262_000); + }); + + test("dotted id honors a configured window on the normalized spelling", () => { + const provider: OcxProviderConfig = { + adapter: "devin", baseUrl: "https://devin.test", + modelContextWindows: { "swe-1-7-medium": 111_000 }, + }; + expect(resolveInputCeiling(provider, "devin", "swe-1.7-medium")).toBe(111_000); + }); + + test("dotted id honors a configured input cap on the normalized spelling", () => { + const provider: OcxProviderConfig = { + adapter: "devin", baseUrl: "https://devin.test", + modelMaxInputTokens: { "swe-1-7": 100_000 }, + }; + expect(resolveInputCeiling(provider, "devin", "swe-1.7")).toBe(100_000); + }); + + test("non-devin adapters stay unresolved", () => { + const other: OcxProviderConfig = { adapter: "openai-chat", baseUrl: "https://example.test/v1" }; + expect(resolveInputCeiling(other, "custom", "swe-2-flash")).toBeNull(); + }); });