Skip to content
Merged
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
23 changes: 21 additions & 2 deletions src/adapters/devin/cloud-direct/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@
* drift) we silently fall back to the chat path so a transient catalog
* outage can't take chat down with it.
*
* Schema (verified against the bundled `extension.js`,
* `exa.codeium_common_pb.ClientModelConfig`):
* Schema (#1/#4/#22 verified against the bundled `extension.js`,
* `exa.codeium_common_pb.ClientModelConfig`; #18 identified from a live
* catalog dump against vendor-known windows; #5 corroborated against the
* public WindsurfAPI `ClientModelConfig` documentation):
*
* GetCascadeModelConfigsResponse {
* #1 client_model_configs: repeated ClientModelConfig
* }
* ClientModelConfig {
* #1 label string
* #4 disabled bool ← the gate this module reads
* #5 supports_images bool ← tri-state: absent stays unknown
* #18 max_input_tokens varint ← per-account context window
* #22 model_uid string ← what `GetChatMessage` accepts
* }
*
Expand Down Expand Up @@ -78,6 +82,15 @@ export interface ModelCatalogEntry {
* degrades: the caller keeps its static fallback instead of reporting zero.
*/
contextWindow?: number;
/**
* Image-input support from `ClientModelConfig` field #5, kept as a
* tri-state: a present `true` asserts text+image support, a present
* `false` asserts text-only, and an OMITTED field stays `undefined`
* (unknown). Deliberately unlike `disabled`, which defaults to false —
* collapsing "never asserted" into "text-only" was the #1796 regression
* (see src/providers/antigravity-models.ts).
*/
supportsImages?: boolean;
}

export interface CacheEntry {
Expand Down Expand Up @@ -113,12 +126,17 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
let modelUid = '';
let disabled = false;
let contextWindow = 0;
let supportsImages: boolean | undefined;
for (const sf of iterFields(f.value as Buffer)) {
if (sf.num === 1 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
label = (sf.value as Buffer).toString('utf8');
} else if (sf.num === 4 && sf.wire === 0) {
// #4 = disabled (bool, varint 0/1)
disabled = sf.value === 1n;
} else if (sf.num === 5 && sf.wire === 0) {
// #5 = supportsImages (bool, varint 0/1). Absent stays unknown — see
// ModelCatalogEntry; do not default it like disabled.
supportsImages = sf.value === 1n;
Comment on lines +136 to +139

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update every mapped adapter document

This changes the shared src/adapters/ catalog contract, but inspection of the commit shows that only structure/adapters/registry.md was updated; the other documents mapped to src/adapters/ in structure/INDEX.md—including runtime.md, the three transport documents, data-planes/inbound-compat.md, providers/cursor.md, and providers/chat-compat.md—remain unchanged. Update every mapped document in this change, or correct the manifest ownership if those documents should not own this area, so the repository's source-to-document contract does not drift.

AGENTS.md reference: src/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

} else if (sf.num === 18 && sf.wire === 0) {
// #18 = max input tokens. Identified by dumping a live catalog and
// reading the varints back against models whose windows are known from
Expand All @@ -135,6 +153,7 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
label: label || modelUid,
disabled,
...(contextWindow > 0 ? { contextWindow } : {}),
...(supportsImages !== undefined ? { supportsImages } : {}),
});
}
}
Expand Down
7 changes: 7 additions & 0 deletions structure/adapters/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ Some adapters share another adapter's routed-tool semantics while retaining inde
`projectDevinCliAuthMode` rewrites any saved row that still names the retired adapter id,
alongside the merge migration that retires the `devin-cli` provider id itself.

Before spending a chat roundtrip the adapter runs a catalog pre-flight:
`src/adapters/devin/cloud-direct/catalog.ts` fetches `GetCascadeModelConfigs` and preserves
`ClientModelConfig` field #4 as the per-account disabled gate, field #18 as the per-account
context window, and field #5 as an optional `supportsImages` tri-state — a present value
asserts image support or its absence, while an omitted field stays unknown (the #1796
precedent).

The registry records those relationships with `contractParent`. A parent relationship does **not** mean the registry recursively constructs a parent adapter and injects it into the child. Azure and MiMo keep owning their existing internal composition. This avoids making production constructors depend on test/conformance needs and keeps this authority refactor behavior-neutral.

Codex Spark retirement removes model-specific exceptions from the Responses adapter, without
Expand Down
32 changes: 32 additions & 0 deletions tests/providers/devin-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,38 @@ describe("devin adapter", () => {
expect(catalog.byUid.get("mystery-model")?.contextWindow).toBeUndefined();
});

test("the catalog parser preserves image support as a tri-state", () => {
// ClientModelConfig #5 is supports_images. encodeVarintField(5, 0) emits
// real bytes ([0x28, 0x00]), so the false case is not an omission case —
// and a genuinely absent field must stay unknown rather than collapse to
// text-only (#1796).
const vision = Buffer.concat([
encodeString(1, "Vision Model"),
encodeVarintField(5, 1),
encodeString(22, "vision-model"),
]);
const textOnly = Buffer.concat([
encodeString(1, "Text Model"),
encodeVarintField(5, 0),
encodeString(22, "text-model"),
]);
const unknown = Buffer.concat([
encodeString(1, "Unknown Model"),
encodeString(22, "unknown-model"),
]);
const catalog = parseCatalogBuffer(
Buffer.concat([encodeMessage(1, vision), encodeMessage(1, textOnly), encodeMessage(1, unknown)]),
"key",
"https://server.codeium.com",
);
expect(catalog.byUid.get("vision-model")?.supportsImages).toBe(true);
// toBe(false), not toBeFalsy: a present 0 asserts text-only.
expect(catalog.byUid.get("text-model")?.supportsImages).toBe(false);
// The entry must exist before its field can be asserted absent.
expect(catalog.byUid.get("unknown-model")).toBeDefined();
expect(catalog.byUid.get("unknown-model")?.supportsImages).toBeUndefined();
});

test("the degraded-mode windows match what Cognition serves", () => {
// This table was wrong for nine of its eleven rows because it had been
// copied from each model's ORIGINAL vendor rather than measured against
Expand Down
Loading