Skip to content
Closed
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
25 changes: 16 additions & 9 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,10 @@ configuration that names the old id is rewritten at startup.
session token doubled and dash-joined in an `Authorization: Basic` header while the protobuf body
keeps one copy, the request envelope goes up uncompressed, and `Metadata` #31 carries a
732-character device fingerprint whose length — not value — the service checks. Inside
`CompletionConfiguration`, #2 is the output cap and #3 is the context window; swapping those two
makes every turn fail with an opaque `invalid_argument`. A temperature of exactly 0 is refused, so
`CompletionConfiguration`, #2 is the output cap and #3 is `max_newlines`, sent at a fixed large
value; swapping those two makes every turn fail with an opaque `invalid_argument`. With no caller
or configured cap, the output cap is the selected model's own ceiling from the catalog, and 8192
only when the catalog is unavailable. A temperature of exactly 0 is refused, so
it is clamped to the smallest accepted value.
- A pre-output 429 with a stated recovery delay is surfaced immediately by default, releasing the
admitted turn's shared capacity. Set `OPENCODEX_DEVIN_STATED_RESET_WAIT_MS` to a positive cumulative
Expand All @@ -591,13 +593,18 @@ configuration that names the old id is rewritten at startup.
- Experimental unofficial bridge; not shown in the dashboard preset by default. See the
[provider guide](/guides/providers/) for login instructions.

For SWE-2, an explicit reasoning effort overrides an effort suffix in the model
id. For example, `swe-2-high` with `medium` selects the native `swe-2-medium` UID;
`xhigh`, `ultra`, and `max` select `swe-2-max`. Values below Medium select Medium
and do not disable SWE-2 reasoning. Without an explicit effort, a suffixed model
id is preserved. This applies through the shared adapter to every Devin account,
whichever login path minted the credential; other model families keep their
existing suffix precedence.
The model id you pick is a model family, and the reasoning effort picks the
variant. With no effort, the family's own default variant is used: `swe-1-7`
selects `swe-1-7-medium` and `swe-2` selects `swe-2-high`. An effort changes only
the effort and lands on the lowest variant at or above it, or the highest one
below when nothing is above, so a missing rung never turns reasoning down:
`swe-1-7` at `high` selects the Max row `swe-1-7`, `swe-2` at `low` selects
`swe-2-medium`, and `kimi-k3` at `medium` selects `kimi-k3-high`. `fast` selects the Fast variant and `1m`, `max-1m` or `none-1m`
the 1M-context variant where the family has one; otherwise they change nothing.
A suffixed id such as `claude-opus-5-high-fast` keeps its variant without an
effort, and with one keeps its Fast and context settings. Resolution never moves
to another family. When the account catalog is unavailable, the effort is
appended to the id instead.

## `azure-openai` (alias: `azure`)

Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@
"devin-adapter.test.ts": "providers",
"devin-cli-authmode-migration.test.ts": "providers",
"devin-effort-ladder.test.ts": "providers",
"devin-family-resolution.test.ts": "providers",
"devin-hardening.test.ts": "providers",
"devin-image-passthrough.test.ts": "providers",
"devin-live-models.test.ts": "providers",
Expand Down
181 changes: 118 additions & 63 deletions src/adapters/devin.ts

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions src/adapters/devin/cloud-direct/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,18 @@
* #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
* #23 model_info ModelInfo { #6 features { #15 supports_thinking },
* #13 max_output_tokens, #23 model_family_uid }
* #30 family_metadata ModelFamilyMetadata { #1 family_label,
* #2 repeated Entry { #1 axis key, #2 { #1 order, #2 name } },
* #3 is_default_model_in_family }
* #31 is_default_model_in_family bool
* }
*
* #23/#30/#31 were read back from a live 267-row catalog (2026-09-27): every
* current row names its family, and each family marks one default member.
* Legacy `MODEL_*` rows carry no #30.
*
* Disabled semantics: TRUE means "this UID exists in the catalog but the
* caller's account/tier cannot run inference against it." BYOK models
* surface as `disabled: false` so users with their own provider keys still
Expand Down Expand Up @@ -91,6 +101,73 @@ export interface ModelCatalogEntry {
* (see src/providers/antigravity-models.ts).
*/
supportsImages?: boolean;
/** ModelInfo #13: the model's own output-token ceiling. */
maxOutputTokens?: number;
/** ModelInfo #6.15. */
supportsThinking?: boolean;
/** ModelInfo #23, e.g. `swe-1.7` for both `swe-1-7` and `swe-1-7-medium`. */
familyUid?: string;
/** ModelFamilyMetadata #1, e.g. `SWE-1.7`. */
familyLabel?: string;
/**
* ModelFamilyMetadata #2: this row's position on each of its family's axes,
* keyed by axis name (`Effort`, `Reasoning Effort`, `Fast Mode`, `1M Context`,
* `Thinking`, ...). Toggle axes such as `Fast Mode` carry only an order.
*/
familyAxes?: Record<string, DevinFamilyAxisValue>;
/** #31 (or #30.3): the member the family selects when nothing is asked for. */
isFamilyDefault?: boolean;
}

export interface DevinFamilyAxisValue {
order: number;
name?: string;
}

/**
* Rows whose catalog entry claims image support that the model does not have.
* Live 2026-09-27: `swe-1-6` answered "NOIMAGE" to a solid red PNG while
* `swe-1-6-fast` and `swe-2-medium` named the colour, so the image is dropped
* server-side without an error. Marking it text-only lets the vision fallback
* describe the image instead of the model silently never seeing it.
*/
const IMAGE_BLIND_UIDS = new Set(['swe-1-6']);

function parseFamilyMetadata(buf: Buffer): { label?: string; axes: Record<string, DevinFamilyAxisValue>; isDefault: boolean } {
let label: string | undefined;
let isDefault = false;
const axes: Record<string, DevinFamilyAxisValue> = {};
for (const f of iterFields(buf)) {
if (f.num === 1 && f.wire === 2 && Buffer.isBuffer(f.value)) label = f.value.toString('utf8');
else if (f.num === 3 && f.wire === 0) isDefault = f.value === 1n;
else if (f.num === 2 && f.wire === 2 && Buffer.isBuffer(f.value)) {
let key = '';
const value: DevinFamilyAxisValue = { order: 0 };
for (const e of iterFields(f.value)) {
if (e.num === 1 && e.wire === 2 && Buffer.isBuffer(e.value)) key = e.value.toString('utf8');
else if (e.num === 2 && e.wire === 2 && Buffer.isBuffer(e.value)) {
for (const v of iterFields(e.value)) {
if (v.num === 1 && v.wire === 0) value.order = Number(v.value);
else if (v.num === 2 && v.wire === 2 && Buffer.isBuffer(v.value)) value.name = v.value.toString('utf8');
}
}
}
if (key) axes[key] = value;
}
}
return { ...(label ? { label } : {}), axes, isDefault };
}

function parseModelInfo(buf: Buffer): { maxOutputTokens?: number; familyUid?: string; supportsThinking?: boolean } {
const out: { maxOutputTokens?: number; familyUid?: string; supportsThinking?: boolean } = {};
for (const f of iterFields(buf)) {
if (f.num === 13 && typeof f.value === 'bigint' && f.value > 0n) out.maxOutputTokens = Number(f.value);
else if (f.num === 23 && f.wire === 2 && Buffer.isBuffer(f.value) && f.value.length > 0) out.familyUid = f.value.toString('utf8');
else if (f.num === 6 && f.wire === 2 && Buffer.isBuffer(f.value)) {
for (const g of iterFields(f.value)) if (g.num === 15 && g.wire === 0) out.supportsThinking = g.value === 1n;
}
}
return out;
}

export interface CacheEntry {
Expand Down Expand Up @@ -127,6 +204,9 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
let disabled = false;
let contextWindow = 0;
let supportsImages: boolean | undefined;
let info: ReturnType<typeof parseModelInfo> = {};
let family: ReturnType<typeof parseFamilyMetadata> | undefined;
let isFamilyDefault = false;
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');
Expand All @@ -145,15 +225,26 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
contextWindow = Number(sf.value);
} else if (sf.num === 22 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
modelUid = (sf.value as Buffer).toString('utf8');
} else if (sf.num === 23 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
info = parseModelInfo(sf.value);
} else if (sf.num === 30 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
family = parseFamilyMetadata(sf.value);
} else if (sf.num === 31 && sf.wire === 0) {
isFamilyDefault = sf.value === 1n;
}
}
if (IMAGE_BLIND_UIDS.has(modelUid)) supportsImages = false;
if (modelUid.length > 0) {
byUid.set(modelUid, {
modelUid,
label: label || modelUid,
disabled,
...(contextWindow > 0 ? { contextWindow } : {}),
...(supportsImages !== undefined ? { supportsImages } : {}),
...info,
...(family?.label ? { familyLabel: family.label } : {}),
...(family ? { familyAxes: family.axes } : {}),
...(isFamilyDefault || family?.isDefault ? { isFamilyDefault: true } : {}),
});
}
}
Expand Down
25 changes: 14 additions & 11 deletions src/adapters/devin/cloud-direct/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,15 @@ function collapseSystemIntoUser(messages: ChatHistoryItem[]): ChatHistoryItem[]
* CompletionConfiguration — mirrors the LS-shipped defaults, lets the caller
* override the obvious knobs.
*/
/** Output cap when the caller named none. */
/** Output cap when neither the caller nor the catalog named one. */
const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
/** Context window when the caller named none. */
const DEFAULT_CONTEXT_WINDOW = 128_000;
/**
* CompletionConfiguration #3 is `max_newlines`, not a token count and not an
* input ceiling: live, a value of 5 did not truncate a 25-line answer. It is
* still sent, at the value every turn has carried, so the request shape the
* service accepts does not change.
*/
const MAX_NEWLINES = 128_000;

/**
* Cognition rejects a temperature of exactly 0 with the same opaque internal
Expand All @@ -353,7 +358,6 @@ function safeTemperature(value: number | undefined): number {

function encodeCompletionConfiguration(opts: {
maxOutputTokens?: number;
maxInputTokens?: number;
temperature?: number;
topK?: number;
topP?: number;
Expand All @@ -365,15 +369,15 @@ function encodeCompletionConfiguration(opts: {
};
// Tag map, verified by building the same turn with a working client and
// diffing the encoded messages field by field: #2 is the OUTPUT cap and #3 is
// the context window. This layout had those two swapped, so a caller asking
// for 32 output tokens put 32 into the context-window field and the request
// came back as an opaque "an internal error occurred" — for every turn, on
// every account, which is why free and paid failed identically. #6 and #11
// are not part of the message the service accepts.
// max_newlines. This layout once had those two swapped, so a caller's output
// cap landed in #3 and a large value in #2, and the request came back as an
// opaque "an internal error occurred" — for every turn, on every account,
// which is why free and paid failed identically. #6 and #11 are not part of
// the message the service accepts.
return Buffer.concat([
encodeVarintField(1, 1),
encodeVarintField(2, opts.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS),
encodeVarintField(3, opts.maxInputTokens ?? DEFAULT_CONTEXT_WINDOW),
encodeVarintField(3, MAX_NEWLINES),
enc64(5, safeTemperature(opts.temperature)),
encodeVarintField(7, opts.topK ?? 40),
enc64(8, opts.topP ?? 1.0),
Expand Down Expand Up @@ -543,7 +547,6 @@ interface BuildArgs {
requestType?: number;
completionOpts?: {
maxOutputTokens?: number;
maxInputTokens?: number;
temperature?: number;
topK?: number;
topP?: number;
Expand Down
Loading
Loading