@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-CWXut3rG.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Client Protocol framing for the Devin CLI.
|
|
3
|
+
*
|
|
4
|
+
* `devin acp` speaks newline-delimited JSON-RPC on stdin/stdout. One ACP session
|
|
5
|
+
* answers one prompt, so a turn is: initialize -> session/new -> session/prompt,
|
|
6
|
+
* with session/update notifications streaming in between and a unary reply to
|
|
7
|
+
* the prompt carrying the stop reason and usage.
|
|
8
|
+
*
|
|
9
|
+
* This module is pure. It never spawns a process and never touches the network,
|
|
10
|
+
* so the framing and the event mapping are testable against captured lines, the
|
|
11
|
+
* same discipline src/adapters/coding-agent/protocol.ts follows for the
|
|
12
|
+
* stream-json CLIs.
|
|
13
|
+
*/
|
|
14
|
+
import type { AdapterEvent, OcxParsedRequest, OcxToolCall, OcxUsage } from "../../types";
|
|
15
|
+
|
|
16
|
+
/** Hard ceiling on a single buffered stdout line. */
|
|
17
|
+
export const MAX_ACP_LINE_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
/** Hard ceiling on total stdout bytes consumed for one turn. */
|
|
19
|
+
export const MAX_ACP_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
export class AcpProtocolError extends Error {
|
|
22
|
+
readonly code = "protocol_error";
|
|
23
|
+
readonly status = 502;
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "AcpProtocolError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ACP_INITIALIZE_ID = 1;
|
|
31
|
+
export const ACP_SESSION_NEW_ID = 2;
|
|
32
|
+
export const ACP_SESSION_PROMPT_ID = 3;
|
|
33
|
+
|
|
34
|
+
export function initializeFrame(clientVersion: string): Record<string, unknown> {
|
|
35
|
+
return {
|
|
36
|
+
jsonrpc: "2.0",
|
|
37
|
+
id: ACP_INITIALIZE_ID,
|
|
38
|
+
method: "initialize",
|
|
39
|
+
params: { protocolVersion: 1, clientInfo: { name: "opencodex", version: clientVersion }, capabilities: {} },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function sessionNewFrame(cwd: string, modelId?: string): Record<string, unknown> {
|
|
44
|
+
const params: Record<string, unknown> = { cwd, mcpServers: [] };
|
|
45
|
+
// The CLI picks its own default when no model is named, which is what an
|
|
46
|
+
// unset or vendor-default selection should do.
|
|
47
|
+
if (modelId) params.model = modelId;
|
|
48
|
+
return { jsonrpc: "2.0", id: ACP_SESSION_NEW_ID, method: "session/new", params };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sessionPromptFrame(sessionId: string, prompt: string): Record<string, unknown> {
|
|
52
|
+
return {
|
|
53
|
+
jsonrpc: "2.0",
|
|
54
|
+
id: ACP_SESSION_PROMPT_ID,
|
|
55
|
+
method: "session/prompt",
|
|
56
|
+
params: { sessionId, prompt: [{ type: "text", text: prompt }] },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Answer a permission request without a human.
|
|
62
|
+
*
|
|
63
|
+
* A headless turn has nobody to approve a tool call, and an unanswered
|
|
64
|
+
* `session/request_permission` stalls the agent until the turn times out. The
|
|
65
|
+
* answer is a refusal by default: this provider runs an agent in the operator's
|
|
66
|
+
* own tree, and auto-approving whatever it asks for would let any prompt that
|
|
67
|
+
* reaches the proxy read, write and execute there. Approval is an explicit
|
|
68
|
+
* operator decision, and only then is an allow-shaped option preferred over
|
|
69
|
+
* positional guessing — the first option in a real prompt is sometimes the
|
|
70
|
+
* rejection.
|
|
71
|
+
*/
|
|
72
|
+
export function permissionResponseFrame(
|
|
73
|
+
id: number | string,
|
|
74
|
+
options: Array<{ optionId?: string; name?: string; kind?: string }> | undefined,
|
|
75
|
+
allowed = false,
|
|
76
|
+
): Record<string, unknown> {
|
|
77
|
+
if (!allowed) {
|
|
78
|
+
return { jsonrpc: "2.0", id, result: { outcome: { outcome: "cancelled" } } };
|
|
79
|
+
}
|
|
80
|
+
const list = options ?? [];
|
|
81
|
+
const allow =
|
|
82
|
+
list.find((o) => typeof o.kind === "string" && /^allow/i.test(o.kind)) ??
|
|
83
|
+
list.find((o) => /allow|accept|yes/i.test(`${o.optionId ?? ""} ${o.name ?? ""}`));
|
|
84
|
+
if (!allow?.optionId) {
|
|
85
|
+
// Nothing offered says "allow". Guessing at `list[0]` here is how an
|
|
86
|
+
// auto-answer selects a rejection and calls it approval.
|
|
87
|
+
return { jsonrpc: "2.0", id, result: { outcome: { outcome: "cancelled" } } };
|
|
88
|
+
}
|
|
89
|
+
return { jsonrpc: "2.0", id, result: { outcome: { outcome: "selected", optionId: allow.optionId } } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Flatten an OcxContext into the single prompt string one ACP session takes.
|
|
94
|
+
*
|
|
95
|
+
* ACP has no multi-message history on session/prompt, so the conversation is
|
|
96
|
+
* projected into labelled blocks. Tool calls and results are rendered rather
|
|
97
|
+
* than dropped, because a turn that omits them loses the thread of a tool loop.
|
|
98
|
+
*/
|
|
99
|
+
export function buildAcpPrompt(parsed: OcxParsedRequest): string {
|
|
100
|
+
const blocks: string[] = [];
|
|
101
|
+
const system = parsed.context.systemPrompt?.filter((line) => line.trim().length > 0).join("\n");
|
|
102
|
+
if (system) blocks.push(fence("System", system));
|
|
103
|
+
for (const message of parsed.context.messages) {
|
|
104
|
+
if (message.role === "toolResult") {
|
|
105
|
+
const body = typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "");
|
|
106
|
+
blocks.push(fence("Tool", `[result id=${message.toolCallId}]\n${body}`));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const parts = typeof message.content === "string" ? [] : message.content;
|
|
110
|
+
let text = typeof message.content === "string"
|
|
111
|
+
? message.content
|
|
112
|
+
: parts.map((p) => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
|
|
113
|
+
if (message.role === "assistant" && Array.isArray(parts)) {
|
|
114
|
+
const calls = parts
|
|
115
|
+
.filter((p): p is OcxToolCall => p.type === "toolCall")
|
|
116
|
+
.map((c) => `[call ${c.name} id=${c.id}]\n${JSON.stringify(c.arguments ?? {})}`)
|
|
117
|
+
.join("\n\n");
|
|
118
|
+
if (calls) text = text ? `${text}\n\n${calls}` : calls;
|
|
119
|
+
}
|
|
120
|
+
if (!text.trim()) continue;
|
|
121
|
+
const label = message.role === "assistant" ? "Assistant" : message.role === "developer" ? "System" : "User";
|
|
122
|
+
blocks.push(fence(label, text));
|
|
123
|
+
}
|
|
124
|
+
if (blocks.length === 0) return "(empty)";
|
|
125
|
+
const joined = blocks.join("\n\n");
|
|
126
|
+
// Keep the oldest turns rather than the newest when trimming: the tail is
|
|
127
|
+
// what the agent is answering.
|
|
128
|
+
return joined.length > MAX_ACP_PROMPT_CHARS
|
|
129
|
+
? `[truncated]\n${joined.slice(joined.length - MAX_ACP_PROMPT_CHARS)}`
|
|
130
|
+
: joined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type AcpTurnOutcome = { stopReason?: string; usage?: OcxUsage };
|
|
134
|
+
|
|
135
|
+
/** ACP stop reasons that mean the turn ended normally. */
|
|
136
|
+
const NATURAL_STOP = new Set(["end_turn", "stop", "completed"]);
|
|
137
|
+
|
|
138
|
+
export function mapAcpStopReason(reason: unknown): string | undefined {
|
|
139
|
+
if (typeof reason !== "string" || NATURAL_STOP.has(reason)) return undefined;
|
|
140
|
+
if (reason === "max_tokens") return "max_tokens";
|
|
141
|
+
return reason;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function mapAcpUsage(raw: unknown): OcxUsage | undefined {
|
|
145
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
146
|
+
const u = raw as Record<string, unknown>;
|
|
147
|
+
const input = typeof u.inputTokens === "number" ? u.inputTokens : 0;
|
|
148
|
+
const output = typeof u.outputTokens === "number" ? u.outputTokens : 0;
|
|
149
|
+
if (input === 0 && output === 0) return undefined;
|
|
150
|
+
const total = typeof u.totalTokens === "number" ? u.totalTokens : input + output;
|
|
151
|
+
return { inputTokens: input, outputTokens: output, ...(total > 0 ? { totalTokens: total } : {}) };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function chunkText(content: unknown): string {
|
|
155
|
+
if (typeof content === "string") return content;
|
|
156
|
+
if (content && typeof content === "object") {
|
|
157
|
+
const text = (content as { text?: unknown }).text;
|
|
158
|
+
if (typeof text === "string") return text;
|
|
159
|
+
}
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Translate one session/update notification into adapter events.
|
|
165
|
+
*
|
|
166
|
+
* Tool lifecycle is explicit in ACP: `tool_call` opens one and
|
|
167
|
+
* `tool_call_update` with a terminal status closes it, so the caller does not
|
|
168
|
+
* have to infer boundaries from interleaving the way a delta-only wire forces.
|
|
169
|
+
*/
|
|
170
|
+
export function acpUpdateToEvents(update: Record<string, unknown>): AdapterEvent[] {
|
|
171
|
+
const kind = update.sessionUpdate;
|
|
172
|
+
if (kind === "agent_message_chunk") {
|
|
173
|
+
const text = chunkText(update.content);
|
|
174
|
+
return text ? [{ type: "text_delta", text }] : [];
|
|
175
|
+
}
|
|
176
|
+
if (kind === "agent_thought_chunk") {
|
|
177
|
+
const text = chunkText(update.content);
|
|
178
|
+
return text ? [{ type: "thinking_delta", thinking: text }] : [];
|
|
179
|
+
}
|
|
180
|
+
// The CLI's own tool calls are NOT client tools. Devin executes them itself
|
|
181
|
+
// inside its session, so emitting tool_call_start here would either fail the
|
|
182
|
+
// turn — the Responses bridge rejects a tool Codex never declared — or ask
|
|
183
|
+
// Codex to run something the agent has already run. Vendor tools stay
|
|
184
|
+
// internal and Codex keeps ownership of mutation, which is the same rule the
|
|
185
|
+
// CodeBuddy and Qoder adapters follow.
|
|
186
|
+
//
|
|
187
|
+
// They are not dropped silently, though. A Devin tool operation that runs
|
|
188
|
+
// longer than the bridge's stall timeout would otherwise look like upstream
|
|
189
|
+
// silence and get the still-working turn aborted, so an internal update
|
|
190
|
+
// becomes a heartbeat: proof of life without a client-visible tool.
|
|
191
|
+
if (kind === "tool_call" || kind === "tool_call_update" || kind === "plan" || kind === "current_mode_update") {
|
|
192
|
+
return [{ type: "heartbeat" }];
|
|
193
|
+
}
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
/** Ceiling on the flattened conversation handed to one ACP prompt. */
|
|
197
|
+
export const MAX_ACP_PROMPT_CHARS = 200_000;
|
|
198
|
+
|
|
199
|
+
/** Fence a block label so a message body cannot forge one. */
|
|
200
|
+
function fence(label: string, body: string): string {
|
|
201
|
+
// A user or tool result that contains a line reading `[System]` would
|
|
202
|
+
// otherwise appear to open a system block in the flattened prompt.
|
|
203
|
+
return `[${label}]\n${body.replace(/^\[(System|User|Assistant|Tool)\]/gm, " $&")}`;
|
|
204
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin CLI adapter: one ACP session per turn over stdio.
|
|
3
|
+
*
|
|
4
|
+
* This is the local half of Devin support. The cloud-direct `devin` adapter
|
|
5
|
+
* talks to Cognition's api-server; this one drives the installed `devin` CLI,
|
|
6
|
+
* which carries its own credentials from `devin auth login`, so the proxy never
|
|
7
|
+
* sees a token for this provider.
|
|
8
|
+
*
|
|
9
|
+
* runTurn-only, like the Cursor and cloud Devin adapters: a JSON-RPC handshake
|
|
10
|
+
* over a child process has no fetch-shaped request to hand to the generic wire
|
|
11
|
+
* path.
|
|
12
|
+
*
|
|
13
|
+
* The child is treated as untrusted and unprivileged. It gets a scoped
|
|
14
|
+
* environment rather than the proxy's, its permission requests are refused
|
|
15
|
+
* unless an operator opted in, and it is reaped rather than merely signalled,
|
|
16
|
+
* because a Devin grandchild that ignores SIGTERM would otherwise keep writing
|
|
17
|
+
* in the operator's tree after the turn returned.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
20
|
+
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types";
|
|
21
|
+
import type { IncomingMeta, ProviderAdapter } from "../base";
|
|
22
|
+
import { baseScopedEnv } from "../coding-agent/turn";
|
|
23
|
+
import {
|
|
24
|
+
ACP_INITIALIZE_ID,
|
|
25
|
+
ACP_SESSION_NEW_ID,
|
|
26
|
+
ACP_SESSION_PROMPT_ID,
|
|
27
|
+
MAX_ACP_LINE_BYTES,
|
|
28
|
+
MAX_ACP_TOTAL_BYTES,
|
|
29
|
+
acpUpdateToEvents,
|
|
30
|
+
buildAcpPrompt,
|
|
31
|
+
initializeFrame,
|
|
32
|
+
mapAcpStopReason,
|
|
33
|
+
mapAcpUsage,
|
|
34
|
+
permissionResponseFrame,
|
|
35
|
+
sessionNewFrame,
|
|
36
|
+
sessionPromptFrame,
|
|
37
|
+
} from "./acp";
|
|
38
|
+
import { DEVIN_CLI_INSTALL_HINT, resolveDevinCliBinary } from "./binary";
|
|
39
|
+
|
|
40
|
+
/** A turn that has not produced a prompt reply by this point is abandoned. */
|
|
41
|
+
const DEVIN_CLI_TURN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
42
|
+
/** Grace between SIGTERM and SIGKILL when reaping the child. */
|
|
43
|
+
const DEVIN_CLI_KILL_GRACE_MS = 2_000;
|
|
44
|
+
/** How long to wait for the child to actually exit before giving up on it. */
|
|
45
|
+
const DEVIN_CLI_REAP_MS = 5_000;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Identity URL for the provider. The CLI does the real transport over stdio;
|
|
49
|
+
* this is only what the configuration records as the destination, and it has to
|
|
50
|
+
* be an http(s) URL because provider config validation rejects other schemes.
|
|
51
|
+
*/
|
|
52
|
+
export const DEVIN_CLI_IDENTITY_URL = "https://cli.devin.ai";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Opt-in for letting the CLI act on the machine.
|
|
56
|
+
*
|
|
57
|
+
* Off by default: this provider runs an agent in the operator's own tree, and a
|
|
58
|
+
* proxy that auto-approves whatever a prompt asks for is a remote shell.
|
|
59
|
+
*/
|
|
60
|
+
const DEVIN_CLI_ALLOW_TOOLS_ENV = "OPENCODEX_DEVIN_CLI_ALLOW_TOOLS";
|
|
61
|
+
|
|
62
|
+
export type DevinCliSpawn = (binary: string, args: string[], options: { cwd: string; env: Record<string, string> }) => ChildProcessWithoutNullStreams;
|
|
63
|
+
|
|
64
|
+
export function devinCliToolsAllowed(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
65
|
+
const raw = env[DEVIN_CLI_ALLOW_TOOLS_ENV]?.trim().toLowerCase();
|
|
66
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createDevinCliAdapter(provider: OcxProviderConfig, deps?: { spawn?: DevinCliSpawn }): ProviderAdapter {
|
|
70
|
+
const spawnChild: DevinCliSpawn = deps?.spawn
|
|
71
|
+
?? ((binary, args, options) => spawn(binary, args, {
|
|
72
|
+
...options,
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
// Give the child its own process group on POSIX so the reap below can
|
|
76
|
+
// signal the whole tree. Devin spawns shells and tools of its own when
|
|
77
|
+
// the operator allows them, and signalling only the direct pid leaves
|
|
78
|
+
// those descendants writing in the operator's tree after the turn ended.
|
|
79
|
+
detached: process.platform !== "win32",
|
|
80
|
+
}) as ChildProcessWithoutNullStreams);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
name: "devin-cli",
|
|
84
|
+
|
|
85
|
+
buildRequest() {
|
|
86
|
+
// Placeholder: this adapter never travels the fetch path. The URL is the
|
|
87
|
+
// provider's identity, not a destination anything connects to.
|
|
88
|
+
return { url: provider.baseUrl || DEVIN_CLI_IDENTITY_URL, method: "POST", headers: {}, body: "" };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async *parseStream(): AsyncGenerator<AdapterEvent> {
|
|
92
|
+
yield { type: "error", message: "Devin CLI adapter uses runTurn; the fetch/parseStream path is disabled." };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async runTurn(parsed: OcxParsedRequest, incoming: IncomingMeta, emit: (event: AdapterEvent) => void) {
|
|
96
|
+
if (incoming.abortSignal?.aborted) {
|
|
97
|
+
emit({ type: "error", message: "Devin CLI turn was aborted before start." });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const binary = resolveDevinCliBinary();
|
|
101
|
+
if (!binary) {
|
|
102
|
+
emit({ type: "error", message: `Devin CLI not found. ${DEVIN_CLI_INSTALL_HINT}` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const modelId = parsed.modelId.includes("/")
|
|
107
|
+
? parsed.modelId.slice(parsed.modelId.lastIndexOf("/") + 1)
|
|
108
|
+
: parsed.modelId;
|
|
109
|
+
const cwd = process.env.OPENCODEX_DEVIN_CLI_CWD?.trim() || process.cwd();
|
|
110
|
+
const toolsAllowed = devinCliToolsAllowed();
|
|
111
|
+
|
|
112
|
+
await new Promise<void>((resolve) => {
|
|
113
|
+
let child: ChildProcessWithoutNullStreams;
|
|
114
|
+
try {
|
|
115
|
+
child = spawnChild(binary, ["acp"], {
|
|
116
|
+
cwd,
|
|
117
|
+
env: {
|
|
118
|
+
// A scoped environment, not the proxy's. The child would
|
|
119
|
+
// otherwise inherit every credential this process holds.
|
|
120
|
+
...baseScopedEnv(),
|
|
121
|
+
NO_COLOR: "1",
|
|
122
|
+
// "normal" is the CLI's own refuse-by-default mode. "ask" reads like
|
|
123
|
+
// the right name for it but is not a value the binary accepts: Devin
|
|
124
|
+
// CLI 3000.10.21 exits 2 with
|
|
125
|
+
// invalid value 'ask' for '--permission-mode <PERMISSION_MODE>'
|
|
126
|
+
// Valid options: normal (auto), accept-edits, dangerous (yolo,
|
|
127
|
+
// bypass), autonomous (requires --sandbox)
|
|
128
|
+
// before answering a single prompt, so every turn on this provider
|
|
129
|
+
// failed with the default (tools not allowed) configuration — the
|
|
130
|
+
// one path most operators are on. Found by running a real turn
|
|
131
|
+
// against an installed, signed-in CLI; no unit test could see it,
|
|
132
|
+
// because the spawn is injected and the fake child accepts anything.
|
|
133
|
+
DEVIN_PERMISSION_MODE: toolsAllowed ? (process.env.DEVIN_PERMISSION_MODE ?? "bypass") : "normal",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
emit({ type: "error", message: `Devin CLI failed to start (${binary}): ${(error as Error).message}. ${DEVIN_CLI_INSTALL_HINT}` });
|
|
138
|
+
return resolve();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let settled = false;
|
|
142
|
+
let closed = false;
|
|
143
|
+
let sawProtocolFrame = false;
|
|
144
|
+
let sawPromptReply = false;
|
|
145
|
+
let buffer = "";
|
|
146
|
+
let totalBytes = 0;
|
|
147
|
+
let usage: OcxUsage | undefined;
|
|
148
|
+
let stopReason: string | undefined;
|
|
149
|
+
let stderrTail = "";
|
|
150
|
+
|
|
151
|
+
const turnTimer = setTimeout(
|
|
152
|
+
() => finish(`Devin CLI turn exceeded ${DEVIN_CLI_TURN_TIMEOUT_MS}ms`),
|
|
153
|
+
DEVIN_CLI_TURN_TIMEOUT_MS,
|
|
154
|
+
);
|
|
155
|
+
const onAbort = () => finish("Devin CLI turn was aborted.");
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reap the child rather than just signalling it, then resolve.
|
|
159
|
+
*
|
|
160
|
+
* `child.killed` only records that a signal was sent. Resolving on that
|
|
161
|
+
* lets a grandchild keep running in the operator's tree after runTurn
|
|
162
|
+
* returned, which is why this waits for `close` and escalates.
|
|
163
|
+
*/
|
|
164
|
+
function reapAndResolve(): void {
|
|
165
|
+
if (closed || child.exitCode !== null || child.signalCode !== null) return resolve();
|
|
166
|
+
let done = false;
|
|
167
|
+
const settle = () => {
|
|
168
|
+
if (done) return;
|
|
169
|
+
done = true;
|
|
170
|
+
clearTimeout(killTimer);
|
|
171
|
+
clearTimeout(reapTimer);
|
|
172
|
+
resolve();
|
|
173
|
+
};
|
|
174
|
+
child.once("close", settle);
|
|
175
|
+
signalTree("SIGTERM");
|
|
176
|
+
const killTimer = setTimeout(() => signalTree("SIGKILL"), DEVIN_CLI_KILL_GRACE_MS);
|
|
177
|
+
const reapTimer = setTimeout(settle, DEVIN_CLI_REAP_MS);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Signal the child's whole process group where the platform has one.
|
|
182
|
+
* Devin launches shells and tools of its own once the operator allows
|
|
183
|
+
* them, and those descendants do not receive a signal aimed at the
|
|
184
|
+
* direct pid. Falls back to the single process when the group send is
|
|
185
|
+
* unavailable or the group is already gone.
|
|
186
|
+
*/
|
|
187
|
+
function signalTree(signal: NodeJS.Signals): void {
|
|
188
|
+
const pid = child.pid;
|
|
189
|
+
if (pid !== undefined && process.platform !== "win32") {
|
|
190
|
+
try {
|
|
191
|
+
process.kill(-pid, signal);
|
|
192
|
+
return;
|
|
193
|
+
} catch { /* no group, or already reaped - fall through */ }
|
|
194
|
+
}
|
|
195
|
+
try { child.kill(signal); } catch { /* already gone */ }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Terminate the turn exactly once, with an error when given a reason. */
|
|
199
|
+
function finish(errorMessage?: string): void {
|
|
200
|
+
if (settled) return;
|
|
201
|
+
settled = true;
|
|
202
|
+
clearTimeout(turnTimer);
|
|
203
|
+
incoming.abortSignal?.removeEventListener("abort", onAbort);
|
|
204
|
+
child.stdout.destroy();
|
|
205
|
+
if (errorMessage) emit({ type: "error", message: errorMessage, ...(usage ? { usage } : {}) });
|
|
206
|
+
else emit({ type: "done", ...(usage ? { usage } : {}), ...(stopReason ? { stopReason } : {}) });
|
|
207
|
+
reapAndResolve();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
incoming.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
211
|
+
// The signal can fire between the pre-spawn check and this listener.
|
|
212
|
+
if (incoming.abortSignal?.aborted) return finish("Devin CLI turn was aborted.");
|
|
213
|
+
|
|
214
|
+
const send = (frame: Record<string, unknown>): void => {
|
|
215
|
+
if (!child.stdin.destroyed) child.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
216
|
+
};
|
|
217
|
+
// EPIPE after the child is killed is an ordinary race, not a crash.
|
|
218
|
+
child.stdin.on("error", () => {});
|
|
219
|
+
|
|
220
|
+
child.on("error", (err) => finish(`Devin CLI failed to start (${binary}): ${err.message}. ${DEVIN_CLI_INSTALL_HINT}`));
|
|
221
|
+
|
|
222
|
+
child.on("close", (code) => {
|
|
223
|
+
closed = true;
|
|
224
|
+
if (settled) return;
|
|
225
|
+
// Flush a final frame that arrived without a trailing newline before
|
|
226
|
+
// deciding the turn failed: the prompt reply carrying usage and the
|
|
227
|
+
// stop reason is often the last line written.
|
|
228
|
+
flush(buffer);
|
|
229
|
+
buffer = "";
|
|
230
|
+
if (settled) return;
|
|
231
|
+
// A close without a prompt reply is a failure, not an empty success.
|
|
232
|
+
const detail = stderrTail.trim().slice(-400);
|
|
233
|
+
finish(
|
|
234
|
+
`Devin CLI exited (code ${code ?? "null"}) before answering the prompt` +
|
|
235
|
+
(detail ? `: ${detail}` : "."),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
child.stderr?.setEncoding("utf8");
|
|
240
|
+
child.stderr?.on("data", (chunk: string) => {
|
|
241
|
+
// Bounded: diagnostics are for the error message, not a buffer to grow.
|
|
242
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
child.stdout.setEncoding("utf8");
|
|
246
|
+
child.stdout.on("data", (chunk: string) => {
|
|
247
|
+
if (settled) return;
|
|
248
|
+
totalBytes += Buffer.byteLength(chunk, "utf8");
|
|
249
|
+
if (totalBytes > MAX_ACP_TOTAL_BYTES) return finish("Devin CLI produced more output than one turn may consume.");
|
|
250
|
+
buffer += chunk;
|
|
251
|
+
let index: number;
|
|
252
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
253
|
+
const line = buffer.slice(0, index);
|
|
254
|
+
buffer = buffer.slice(index + 1);
|
|
255
|
+
flush(line);
|
|
256
|
+
if (settled) return;
|
|
257
|
+
}
|
|
258
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_ACP_LINE_BYTES) {
|
|
259
|
+
finish("Devin CLI emitted a single line larger than the frame cap.");
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// The prompt reply carrying usage and the stop reason is often the last
|
|
264
|
+
// thing written, and it is not guaranteed to end with a newline. Flush
|
|
265
|
+
// the remainder when the stream ends rather than waiting for the child
|
|
266
|
+
// to exit and then calling a complete turn a failure.
|
|
267
|
+
child.stdout.on("end", () => {
|
|
268
|
+
const tail = buffer;
|
|
269
|
+
buffer = "";
|
|
270
|
+
flush(tail);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
function flush(raw: string): void {
|
|
274
|
+
const line = raw.trim();
|
|
275
|
+
if (!line || settled) return;
|
|
276
|
+
let frame: Record<string, unknown>;
|
|
277
|
+
try {
|
|
278
|
+
frame = JSON.parse(line) as Record<string, unknown>;
|
|
279
|
+
} catch {
|
|
280
|
+
// The CLI prints a banner before the protocol starts, so plain text
|
|
281
|
+
// is expected up to the first valid frame. After that the stream is
|
|
282
|
+
// protocol, and a line that is shaped like a frame but does not
|
|
283
|
+
// parse is corruption: dropping it silently loses a session/update
|
|
284
|
+
// or lets the turn wait out the timeout for a reply that already
|
|
285
|
+
// arrived damaged.
|
|
286
|
+
if (sawProtocolFrame || line.startsWith("{")) {
|
|
287
|
+
finish("Devin CLI emitted a malformed ACP frame.");
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
sawProtocolFrame = true;
|
|
292
|
+
handle(frame);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** JSON-RPC ids are allowed to come back as strings. */
|
|
296
|
+
const idOf = (value: unknown): number | undefined => {
|
|
297
|
+
if (typeof value === "number") return value;
|
|
298
|
+
if (typeof value === "string" && /^\d+$/.test(value)) return Number(value);
|
|
299
|
+
return undefined;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
function handle(frame: Record<string, unknown>): void {
|
|
303
|
+
if (settled) return;
|
|
304
|
+
const id = idOf(frame.id);
|
|
305
|
+
const error = frame.error as { message?: string } | undefined;
|
|
306
|
+
|
|
307
|
+
if (id === ACP_INITIALIZE_ID) {
|
|
308
|
+
if (error) return finish(`Devin CLI initialize failed: ${error.message ?? "unknown error"}`);
|
|
309
|
+
send(sessionNewFrame(cwd, modelId));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (id === ACP_SESSION_NEW_ID) {
|
|
313
|
+
if (error) return finish(`Devin CLI session/new failed: ${error.message ?? "unknown error"}`);
|
|
314
|
+
const sessionId = (frame.result as { sessionId?: string } | undefined)?.sessionId;
|
|
315
|
+
if (!sessionId) return finish("Devin CLI session/new returned no sessionId.");
|
|
316
|
+
send(sessionPromptFrame(sessionId, buildAcpPrompt(parsed)));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (frame.method === "session/request_permission" && frame.id != null) {
|
|
320
|
+
const params = frame.params as { options?: Array<{ optionId?: string; name?: string; kind?: string }> } | undefined;
|
|
321
|
+
send(permissionResponseFrame(frame.id as number | string, params?.options, toolsAllowed));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (frame.method === "session/update") {
|
|
325
|
+
const update = (frame.params as { update?: Record<string, unknown> } | undefined)?.update;
|
|
326
|
+
if (!update) return;
|
|
327
|
+
for (const event of acpUpdateToEvents(update)) emit(event);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (id === ACP_SESSION_PROMPT_ID) {
|
|
331
|
+
if (error) return finish(`Devin CLI session/prompt failed: ${error.message ?? "unknown error"}`);
|
|
332
|
+
sawPromptReply = true;
|
|
333
|
+
const result = frame.result as { stopReason?: unknown; usage?: unknown } | undefined;
|
|
334
|
+
usage = mapAcpUsage(result?.usage) ?? usage;
|
|
335
|
+
stopReason = mapAcpStopReason(result?.stopReason);
|
|
336
|
+
finish();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
void sawPromptReply;
|
|
341
|
+
send(initializeFrame(process.env.OPENCODEX_VERSION ?? "0.0.0"));
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate the Devin CLI.
|
|
3
|
+
*
|
|
4
|
+
* The official installer (`curl -fsSL https://cli.devin.ai/install.sh | bash`)
|
|
5
|
+
* and the Homebrew cask both drop the binary in one of a small set of places.
|
|
6
|
+
* The environment override comes first so an operator can point at a specific
|
|
7
|
+
* build without touching PATH, and PATH is the last resort rather than the
|
|
8
|
+
* first so a shadowed name cannot silently win.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { delimiter, join } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
|
|
14
|
+
export const DEVIN_CLI_BIN_ENV = "OPENCODEX_DEVIN_CLI_BIN";
|
|
15
|
+
|
|
16
|
+
export const DEVIN_CLI_INSTALL_HINT =
|
|
17
|
+
"Install the Devin CLI with `curl -fsSL https://cli.devin.ai/install.sh | bash` or `brew install --cask devin-cli`, then run `devin auth login`.";
|
|
18
|
+
|
|
19
|
+
let cached: string | undefined;
|
|
20
|
+
|
|
21
|
+
/** Reset the discovery cache (tests, or an explicit re-check after an install). */
|
|
22
|
+
export function clearDevinCliBinaryCache(): void {
|
|
23
|
+
cached = undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function candidatePaths(home: string): string[] {
|
|
27
|
+
return [
|
|
28
|
+
join(home, "AppData", "Local", "Microsoft", "WinGet", "Links", "devin.exe"),
|
|
29
|
+
join(home, ".local", "share", "devin", "bin", "devin"),
|
|
30
|
+
join(home, ".devin", "bin", "devin"),
|
|
31
|
+
join(home, ".local", "bin", "devin"),
|
|
32
|
+
"/opt/homebrew/bin/devin",
|
|
33
|
+
"/usr/local/bin/devin",
|
|
34
|
+
"/usr/bin/devin",
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fromPath(exists: (p: string) => boolean): string | undefined {
|
|
39
|
+
const pathVar = process.env.PATH ?? "";
|
|
40
|
+
for (const dir of pathVar.split(delimiter)) {
|
|
41
|
+
if (!dir) continue;
|
|
42
|
+
for (const name of ["devin", "devin.exe"]) {
|
|
43
|
+
const full = join(dir, name);
|
|
44
|
+
if (exists(full)) return full;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the executable, or undefined when it is not installed.
|
|
52
|
+
*
|
|
53
|
+
* `exists` and `home` are seams so the resolution order can be tested without
|
|
54
|
+
* depending on what happens to be installed on the machine running the tests.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveDevinCliBinary(opts?: { exists?: (p: string) => boolean; home?: string; useCache?: boolean }): string | undefined {
|
|
57
|
+
const exists = opts?.exists ?? existsSync;
|
|
58
|
+
const useCache = opts?.useCache ?? opts === undefined;
|
|
59
|
+
if (useCache && cached) return cached;
|
|
60
|
+
const override = process.env[DEVIN_CLI_BIN_ENV]?.trim();
|
|
61
|
+
if (override) {
|
|
62
|
+
if (useCache) cached = override;
|
|
63
|
+
return override;
|
|
64
|
+
}
|
|
65
|
+
const home = opts?.home ?? homedir();
|
|
66
|
+
const found = candidatePaths(home).find((p) => exists(p)) ?? fromPath(exists);
|
|
67
|
+
if (found && useCache) cached = found;
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Models the Devin CLI accepts on `session/new`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI picks its own default when no model is named, so this roster exists
|
|
5
|
+
* for the picker rather than as a gate. It is a static list on purpose: ACP has
|
|
6
|
+
* no discovery call, and the vendor roster moves faster than a pinned copy
|
|
7
|
+
* would, so an unknown id is passed through to the CLI to accept or refuse.
|
|
8
|
+
*/
|
|
9
|
+
export const DEVIN_CLI_DEFAULT_MODEL = "swe-2";
|
|
10
|
+
|
|
11
|
+
export const DEVIN_CLI_MODELS = [
|
|
12
|
+
"swe-2",
|
|
13
|
+
"swe-2-high",
|
|
14
|
+
"claude-opus-5-medium",
|
|
15
|
+
"claude-fable-5-1-medium",
|
|
16
|
+
"claude-sonnet-5-medium",
|
|
17
|
+
"gpt-6-astra-medium",
|
|
18
|
+
"gpt-5-6-sol-medium",
|
|
19
|
+
"gemini-3-8-flash-medium",
|
|
20
|
+
"glm-5-3-high",
|
|
21
|
+
"glm-5-3-low",
|
|
22
|
+
"kimi-k3-high",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Context windows for the CLI roster, in the same effort-suffixed ids the CLI
|
|
27
|
+
* accepts.
|
|
28
|
+
*
|
|
29
|
+
* Without this the picker fell back to the 128k default for every Devin CLI
|
|
30
|
+
* model, including `swe-2`, which is the roster's own default — so the one
|
|
31
|
+
* model most sessions ran reported less than half its real window.
|
|
32
|
+
*
|
|
33
|
+
* The numbers come from Cognition's `GetCascadeModelConfigs` catalog
|
|
34
|
+
* (`ClientModelConfig` field #18), which is the only first-party source: the
|
|
35
|
+
* Devin CLI and Desktop model pages, the SWE-2 announcement, and the Windsurf
|
|
36
|
+
* model reference all list these models without a window. The CLI is a separate
|
|
37
|
+
* product from the cloud, but Cognition documents the same models on both and
|
|
38
|
+
* describes no per-surface difference — the SWE-2 announcement ships it to
|
|
39
|
+
* Desktop, CLI, Web, and Fusion in one sentence — so the catalog's figure is
|
|
40
|
+
* used for both rather than inventing a second table.
|
|
41
|
+
*
|
|
42
|
+
* ACP has no discovery call, so unlike the cloud provider this cannot be
|
|
43
|
+
* refreshed live; it needs updating when the roster above does.
|
|
44
|
+
*/
|
|
45
|
+
export const DEVIN_CLI_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
46
|
+
"swe-2": 262_000,
|
|
47
|
+
"swe-2-high": 262_000,
|
|
48
|
+
"claude-opus-5-medium": 1_000_000,
|
|
49
|
+
"claude-fable-5-1-medium": 1_000_000,
|
|
50
|
+
"claude-sonnet-5-medium": 1_000_000,
|
|
51
|
+
"gpt-6-astra-medium": 1_000_000,
|
|
52
|
+
"gpt-5-6-sol-medium": 1_000_000,
|
|
53
|
+
"gemini-3-8-flash-medium": 1_048_576,
|
|
54
|
+
"glm-5-3-high": 1_048_576,
|
|
55
|
+
"glm-5-3-low": 1_048_576,
|
|
56
|
+
"kimi-k3-high": 1_048_576,
|
|
57
|
+
};
|