@bitkyc08/opencodex 2.69.0 → 2.70.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/App-_MrbTrBe.js +51 -0
- package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
- package/gui/dist/assets/index-DKrO2eF3.js +86 -0
- package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-model-contract.ts +119 -0
- package/src/adapters/anthropic.ts +14 -83
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +542 -0
- package/src/adapters/command-code-tool-text.ts +99 -67
- package/src/adapters/command-code.ts +5 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
- package/src/adapters/devin/cloud-direct/chat.ts +78 -48
- package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
- package/src/adapters/devin/context-overflow.ts +75 -0
- package/src/adapters/devin/live-models.ts +174 -13
- package/src/adapters/devin/reasoning-signature.ts +100 -0
- package/src/adapters/devin.ts +310 -117
- package/src/adapters/kiro/payload.ts +10 -10
- package/src/adapters/kiro-images.ts +58 -7
- package/src/adapters/openai-chat/passthrough.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/bridge/response-json.ts +15 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/cli/account-extended.ts +31 -1
- package/src/cli/account.ts +3 -0
- package/src/cli/capabilities.ts +13 -0
- package/src/cli/client-start-fence.ts +36 -0
- package/src/cli/dispatch.ts +3 -2
- package/src/cli/index.ts +98 -117
- package/src/cli/provider-runtime.ts +23 -2
- package/src/cli/provider.ts +3 -0
- package/src/cli/resolve.ts +18 -0
- package/src/cli/start-owner-fence.ts +30 -0
- package/src/cli/stop-approval.ts +13 -1
- package/src/cli/stop-restore.ts +73 -0
- package/src/cli/tray-proxy.ts +221 -18
- package/src/cli/version-skew.ts +25 -23
- package/src/client/runtime.ts +8 -4
- package/src/codex/auth-api/account-list.ts +11 -5
- package/src/codex/auth-api/login-flow.ts +5 -3
- package/src/codex/auth-api/main-account-probe.ts +186 -110
- package/src/codex/auth-api/pool-mode-gate.ts +61 -7
- package/src/codex/auth-api/pool-quota-probe.ts +142 -90
- package/src/codex/auth-api/reset-credit-service.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +3 -3
- package/src/codex/catalog/provider-models.ts +2 -0
- package/src/codex/low-quota-events.ts +28 -0
- package/src/codex/low-quota-observer.ts +23 -0
- package/src/codex/low-quota-protection.ts +272 -0
- package/src/codex/quota-query-backoff.ts +148 -0
- package/src/codex/quota.ts +7 -2
- package/src/config/diagnostics.ts +10 -1
- package/src/config/load-degrade.ts +17 -0
- package/src/config/mutation-lock.ts +3 -4
- package/src/config/process-state.ts +1 -1
- package/src/config/provider-validation.ts +22 -0
- package/src/config/schema/blocked-model-redirects.ts +17 -0
- package/src/config/schema/config-schema.ts +4 -1
- package/src/config/schema/leaf-validators.ts +33 -2
- package/src/config/serving-runtimes.ts +412 -0
- package/src/generated/compatibility-version.json +211 -115
- package/src/generated/model-metadata.ts +3 -3
- package/src/images/loop.ts +3 -0
- package/src/lib/errors.ts +2 -1
- package/src/lib/handled-signal-exit.ts +23 -0
- package/src/lib/request-failure-model.ts +1 -0
- package/src/lib/self-launch-argv.ts +9 -0
- package/src/lib/shadow-call.ts +5 -2
- package/src/lib/strict-semver.ts +26 -0
- package/src/lib/transient-service-path.ts +16 -0
- package/src/lib/windows-service-wrappers.ts +10 -6
- package/src/lib/winsw.ts +3 -1
- package/src/oauth/anthropic-model-routes.ts +105 -0
- package/src/oauth/anthropic-routing.ts +85 -48
- package/src/oauth/devin.ts +135 -5
- package/src/oauth/index.ts +20 -3
- package/src/oauth/kiro-terminal-failover.ts +23 -9
- package/src/oauth/pool-settings-capability.ts +10 -2
- package/src/oauth/store.ts +1 -1
- package/src/protocols/encoders/adapter-events.ts +3 -1
- package/src/providers/github-copilot-context.ts +27 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/quota/account-cache.ts +8 -3
- package/src/providers/quota/devin.ts +286 -0
- package/src/providers/quota/vendor-probes-oauth.ts +6 -2
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +1 -1
- package/src/providers/registry/model-seeds.ts +5 -2
- package/src/responses/compaction-images.ts +27 -0
- package/src/router.ts +104 -16
- package/src/routing/analytics.ts +1 -0
- package/src/server/auth-cors.ts +9 -1
- package/src/server/background-lifecycle.ts +21 -10
- package/src/server/chat-completions.ts +2 -1
- package/src/server/chat-native.ts +1 -0
- package/src/server/index.ts +4 -8
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/management/context.ts +3 -0
- package/src/server/management/low-quota-routes.ts +14 -0
- package/src/server/management/oauth-account-routes.ts +21 -1
- package/src/server/management/provider-overwrite-carry.ts +79 -6
- package/src/server/management/provider-routes.ts +41 -28
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management-api.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +3 -0
- package/src/server/responses/adapter-delivery.ts +3 -0
- package/src/server/responses/adapter-dispatch.ts +20 -4
- package/src/server/responses/antigravity-validation-refusal.ts +20 -0
- package/src/server/responses/core-auth.ts +1 -0
- package/src/server/responses/core-codex-account.ts +1 -0
- package/src/server/responses/core-normalize.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +8 -1
- package/src/server/responses/request-sidecar-auth.ts +2 -0
- package/src/server/responses/request-transport.ts +27 -10
- package/src/server/responses/response-effects.ts +4 -1
- package/src/server/responses/run-turn-execution.ts +126 -42
- package/src/server/responses/sidecar-execution.ts +4 -1
- package/src/service/diagnostics.ts +3 -1
- package/src/service/guarded-manager-target.ts +39 -12
- package/src/service/launchd.ts +14 -6
- package/src/service/repair.ts +6 -1
- package/src/service/service-child-ownership.ts +168 -0
- package/src/service/state.ts +22 -4
- package/src/service/systemd.ts +2 -2
- package/src/service/windows-guarded-manager.ts +530 -0
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service.ts +2 -0
- package/src/types/config.ts +26 -0
- package/src/types/provider.ts +12 -0
- package/src/types/request.ts +2 -0
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/vision/anthropic-describe.ts +2 -1
- package/src/web-search/anthropic-executor.ts +2 -1
- package/src/web-search/loop.ts +1 -0
- package/gui/dist/assets/App-DDbgOTh6.js +0 -51
- package/gui/dist/assets/index-CuaBJkG4.js +0 -86
- package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
package/src/adapters/devin.ts
CHANGED
|
@@ -10,13 +10,14 @@ import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, Ocx
|
|
|
10
10
|
import { namespacedToolName } from "../types";
|
|
11
11
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
12
12
|
import { streamChatEventsWithResetRetry, devinStatedResetWaitMs, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
|
|
13
|
-
import type { ContentPart } from "./devin/cloud-direct/chat";
|
|
14
|
-
import { getCachedCatalog, type CacheEntry } from "./devin/cloud-direct/catalog";
|
|
15
|
-
import { collapseDevinModelUid } from "./devin/live-models";
|
|
13
|
+
import type { CloudChatEvent, ContentPart } from "./devin/cloud-direct/chat";
|
|
14
|
+
import { getCachedCatalog, type CacheEntry, type ModelCatalogEntry } from "./devin/cloud-direct/catalog";
|
|
15
|
+
import { collapseDevinModelUid, devinFamiliesOf, devinFamilyBaseId, selectDevinFamilyMember, type DevinVariantRequest } from "./devin/live-models";
|
|
16
16
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
17
17
|
import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
|
|
18
|
-
import {
|
|
18
|
+
import { devinAssistantReasoning, encodeDevinSignature, hasAnthropicSignature } from "./devin/reasoning-signature";
|
|
19
19
|
import { SendBudgetExhaustedError } from "../lib/upstream-retry";
|
|
20
|
+
import { devinContextOverflowEvent, isDevinHistoryOverflow } from "./devin/context-overflow";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Combine two usage frames from one turn by keeping the larger count per field.
|
|
@@ -54,6 +55,49 @@ export function mergeDevinUsage(previous: OcxUsage, next: OcxUsage): OcxUsage {
|
|
|
54
55
|
*/
|
|
55
56
|
const DEVIN_CLIENT_CLOSED_MESSAGE = "client closed request";
|
|
56
57
|
|
|
58
|
+
/** Below the bridge's upstream stall deadline, so held reasoning never reads as a stall. */
|
|
59
|
+
const HELD_REASONING_HEARTBEAT_MS = 15_000;
|
|
60
|
+
/** Once either limit is crossed, forward the signed attempt and disable fallback. */
|
|
61
|
+
const HELD_REASONING_MAX_EVENTS = 1_024;
|
|
62
|
+
const HELD_REASONING_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
63
|
+
|
|
64
|
+
type DevinUsageEvent = Extract<CloudChatEvent, { kind: "usage" }>;
|
|
65
|
+
|
|
66
|
+
function toOcxDevinUsage(event: DevinUsageEvent): OcxUsage {
|
|
67
|
+
const total = event.totalTokens ?? ((event.promptTokens ?? 0) + (event.completionTokens ?? 0));
|
|
68
|
+
return {
|
|
69
|
+
inputTokens: event.promptTokens ?? 0,
|
|
70
|
+
outputTokens: event.completionTokens ?? 0,
|
|
71
|
+
...(total > 0 ? { totalTokens: total } : {}),
|
|
72
|
+
...(event.cachedInputTokens !== undefined ? { cachedInputTokens: event.cachedInputTokens } : {}),
|
|
73
|
+
...(event.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: event.cacheCreationInputTokens } : {}),
|
|
74
|
+
...(event.reasoningTokens !== undefined ? { reasoningOutputTokens: event.reasoningTokens } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toCloudDevinUsage(usage: OcxUsage): DevinUsageEvent {
|
|
79
|
+
return {
|
|
80
|
+
kind: "usage",
|
|
81
|
+
promptTokens: usage.inputTokens,
|
|
82
|
+
completionTokens: usage.outputTokens,
|
|
83
|
+
totalTokens: usage.totalTokens,
|
|
84
|
+
cachedInputTokens: usage.cachedInputTokens,
|
|
85
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
86
|
+
reasoningTokens: usage.reasoningOutputTokens,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The retry's cumulative usage plus the refused attempt's final counts. */
|
|
91
|
+
function addDevinUsage(event: DevinUsageEvent, prior: DevinUsageEvent): DevinUsageEvent {
|
|
92
|
+
const sum = (a?: number, b?: number) => (a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0));
|
|
93
|
+
const out: DevinUsageEvent = { ...event };
|
|
94
|
+
for (const key of ["promptTokens", "completionTokens", "totalTokens", "cachedInputTokens", "cacheCreationInputTokens", "reasoningTokens"] as const) {
|
|
95
|
+
const value = sum(event[key], prior[key]);
|
|
96
|
+
if (value !== undefined) out[key] = value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
/** Map a cloud-direct failure onto the structured fields the error event carries. */
|
|
58
102
|
export function devinErrorClassification(error: unknown): { status?: number; errorType?: string; retryable?: boolean } {
|
|
59
103
|
const status = error instanceof CloudChatError ? error.status : undefined;
|
|
@@ -131,7 +175,9 @@ const SWE2_EFFORT: Record<string, "medium" | "high" | "max"> = {
|
|
|
131
175
|
|
|
132
176
|
/**
|
|
133
177
|
* Resolve an explicit effort onto a SWE-2 lane, or undefined when this is not a
|
|
134
|
-
* SWE-2 id or the caller named no usable effort.
|
|
178
|
+
* SWE-2 id or the caller named no usable effort. Reached only when the catalog
|
|
179
|
+
* is unavailable or lacks family metadata; otherwise the catalog's SWE-2 family
|
|
180
|
+
* rows decide, with the same rounding. Undefined leaves every existing
|
|
135
181
|
* path untouched, which is what keeps other model families on suffix precedence.
|
|
136
182
|
*/
|
|
137
183
|
function resolveSwe2Variant(modelId: string, reasoningEffort?: string): string | undefined {
|
|
@@ -140,17 +186,92 @@ function resolveSwe2Variant(modelId: string, reasoningEffort?: string): string |
|
|
|
140
186
|
return mapped ? `swe-2-${mapped}` : undefined;
|
|
141
187
|
}
|
|
142
188
|
|
|
189
|
+
const VARIANT_EFFORT_TOKENS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
190
|
+
const EFFORT_ALIASES: Record<string, string> = { off: "none", ultra: "max" };
|
|
191
|
+
|
|
143
192
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
193
|
+
* Read hyphen-separated variant tokens as a request. Any token outside the
|
|
194
|
+
* vocabulary voids the whole value, so an unknown effort changes nothing
|
|
195
|
+
* rather than half-applying. `priority` is the uid spelling of Fast Mode on
|
|
196
|
+
* the GPT rows; a caller never sends it as effort (see CALLER_EFFORT_VALUES).
|
|
197
|
+
*/
|
|
198
|
+
function variantRequestOf(tokens: string[], allowPriority: boolean): DevinVariantRequest | undefined {
|
|
199
|
+
const request: DevinVariantRequest = {};
|
|
200
|
+
for (const raw of tokens) {
|
|
201
|
+
const token = EFFORT_ALIASES[raw] ?? raw;
|
|
202
|
+
if (VARIANT_EFFORT_TOKENS.has(token)) request.effort = token;
|
|
203
|
+
else if (token === "fast" || (allowPriority && token === "priority")) request.fast = true;
|
|
204
|
+
else if (token === "1m") request.longContext = true;
|
|
205
|
+
else return undefined;
|
|
206
|
+
}
|
|
207
|
+
return request;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function callerVariantRequest(reasoningEffort: string | undefined): DevinVariantRequest | undefined {
|
|
211
|
+
const value = reasoningEffort?.toLowerCase();
|
|
212
|
+
if (!value) return undefined;
|
|
213
|
+
const aliased = EFFORT_ALIASES[value] ?? value;
|
|
214
|
+
if (!CALLER_EFFORT_VALUES.has(aliased) && aliased !== "minimal") return undefined;
|
|
215
|
+
return variantRequestOf(aliased.split("-"), false);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve through the catalog's family metadata (ClientModelConfig #23/#30/#31).
|
|
220
|
+
* Returns undefined when the id belongs to no family, which leaves the
|
|
221
|
+
* suffix-based path for legacy rows and catalogs without that metadata.
|
|
147
222
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
223
|
+
* The family id wins over an identical row uid on purpose: bare `swe-1-7` is
|
|
224
|
+
* the Max row while the family default is `swe-1-7-medium`, and `glm-5-2` is
|
|
225
|
+
* both the family id and its default row. A caller naming the family means the
|
|
226
|
+
* family, so no effort selects the default member and an effort moves only the
|
|
227
|
+
* effort axis. A caller naming a member row keeps that row's other axes.
|
|
228
|
+
*/
|
|
229
|
+
function resolveFamilyUid(catalog: CacheEntry, modelId: string, reasoningEffort?: string): string | undefined {
|
|
230
|
+
const families = devinFamiliesOf(catalog);
|
|
231
|
+
const caller = callerVariantRequest(reasoningEffort);
|
|
232
|
+
let members = families.get(modelId);
|
|
233
|
+
let anchor: ModelCatalogEntry | undefined;
|
|
234
|
+
let fromId: DevinVariantRequest = {};
|
|
235
|
+
if (!members) {
|
|
236
|
+
const row = catalog.byUid.get(modelId);
|
|
237
|
+
if (row?.familyUid) {
|
|
238
|
+
members = families.get(devinFamilyBaseId(row.familyUid));
|
|
239
|
+
if (members) anchor = row;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!members) {
|
|
243
|
+
// A suffixed spelling the catalog does not list (`swe-1-7-high`) still
|
|
244
|
+
// names a family; its suffix is the request.
|
|
245
|
+
const base = collapseDevinModelUid(modelId);
|
|
246
|
+
const suffix = base !== modelId ? variantRequestOf(modelId.slice(base.length + 1).split("-"), true) : undefined;
|
|
247
|
+
members = suffix ? families.get(base) : undefined;
|
|
248
|
+
if (suffix) fromId = suffix;
|
|
249
|
+
}
|
|
250
|
+
if (!members) return undefined;
|
|
251
|
+
if (anchor && !caller) return anchor.modelUid;
|
|
252
|
+
const request: DevinVariantRequest = {
|
|
253
|
+
...fromId,
|
|
254
|
+
...(caller?.effort ? { effort: caller.effort } : {}),
|
|
255
|
+
...(caller?.fast || fromId.fast ? { fast: true } : {}),
|
|
256
|
+
...(caller?.longContext || fromId.longContext ? { longContext: true } : {}),
|
|
257
|
+
};
|
|
258
|
+
// A disabled row the caller named keeps its uid when the request still lands
|
|
259
|
+
// on it (`swe-2-max` asked for at `max`), so the chat preflight reports that
|
|
260
|
+
// row's tier refusal instead of quietly serving a different tier. A request
|
|
261
|
+
// for a different variant still passes over disabled rows.
|
|
262
|
+
if (anchor?.disabled && selectDevinFamilyMember(members, request, anchor, { includeDisabled: true }) === anchor) {
|
|
263
|
+
return anchor.modelUid;
|
|
264
|
+
}
|
|
265
|
+
return selectDevinFamilyMember(members, request, anchor)?.modelUid;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the wire model UID using the live catalog as the source of truth.
|
|
151
270
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
271
|
+
* With family metadata in the catalog, resolution picks a family member by
|
|
272
|
+
* axis (see resolveFamilyUid). Without it — legacy rows, an older catalog, or
|
|
273
|
+
* no catalog at all — the id is resolved by suffix: an exact or suffixed UID is
|
|
274
|
+
* kept, otherwise the reasoning effort (or `medium`) is appended.
|
|
154
275
|
*/
|
|
155
276
|
async function resolveWireModelUid(
|
|
156
277
|
rawModelId: string,
|
|
@@ -160,28 +281,33 @@ async function resolveWireModelUid(
|
|
|
160
281
|
catalog?: CacheEntry | null,
|
|
161
282
|
): Promise<string> {
|
|
162
283
|
const modelId = normalizeDevinModelId(rawModelId);
|
|
284
|
+
// Callers that already read the catalog this turn pass it in; an explicit
|
|
285
|
+
// null records a failed lookup and must not trigger a same-turn retry —
|
|
286
|
+
// failures are not cached, so re-reading would only pay another timeout.
|
|
287
|
+
const entry = catalog !== undefined ? catalog : await getCachedCatalog(apiKey, host);
|
|
288
|
+
if (entry) {
|
|
289
|
+
const fromFamily = resolveFamilyUid(entry, modelId, reasoningEffort);
|
|
290
|
+
if (fromFamily) return fromFamily;
|
|
291
|
+
}
|
|
163
292
|
// Explicit effort wins over a suffix the picker already baked into the id, so
|
|
164
293
|
// `swe-2-high` asked for at `medium` becomes `swe-2-medium` instead of ignoring
|
|
165
294
|
// the caller. Runs before the shortcut below, which would otherwise return early.
|
|
166
295
|
const swe2 = resolveSwe2Variant(modelId, reasoningEffort);
|
|
167
296
|
if (swe2) return swe2;
|
|
168
297
|
if (hasEffortSuffix(modelId)) return modelId;
|
|
169
|
-
|
|
170
|
-
// null records a failed lookup and must not trigger a same-turn retry —
|
|
171
|
-
// failures are not cached, so re-reading would only pay another timeout.
|
|
172
|
-
const entry = catalog !== undefined ? catalog : await getCachedCatalog(apiKey, host);
|
|
298
|
+
const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
|
|
173
299
|
if (entry) {
|
|
174
300
|
if (entry.byUid.has(modelId)) return modelId;
|
|
175
|
-
const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
|
|
176
301
|
const suffixed = `${modelId}-${effort}`;
|
|
177
302
|
if (entry.byUid.has(suffixed)) return suffixed;
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
303
|
+
// Any enabled variant of this exact base. The base must match after
|
|
304
|
+
// collapsing, not as a string prefix: `claude-opus-5-` prefixes
|
|
305
|
+
// `claude-opus-5-5-low` and `gpt-5-4-` prefixes `gpt-5-4-mini-low`.
|
|
306
|
+
for (const [uid, row] of entry.byUid) {
|
|
307
|
+
if (!row.disabled && uid !== modelId && collapseDevinModelUid(uid) === modelId) return uid;
|
|
181
308
|
}
|
|
182
309
|
}
|
|
183
310
|
// Degraded mode: append the default effort suffix.
|
|
184
|
-
const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
|
|
185
311
|
return `${modelId}-${effort}`;
|
|
186
312
|
}
|
|
187
313
|
|
|
@@ -198,7 +324,9 @@ const positiveTokenCount = (value: unknown): number | undefined =>
|
|
|
198
324
|
/**
|
|
199
325
|
* Read a per-model token count for the exact UID selected for this turn.
|
|
200
326
|
*
|
|
201
|
-
* Tries the selected UID
|
|
327
|
+
* Tries the selected UID, then its catalog family id (the picker id, e.g.
|
|
328
|
+
* `claude-sonnet-4-6` for `claude-sonnet-4-6-thinking`), then its collapsed
|
|
329
|
+
* base id, preferring the
|
|
202
330
|
* canonical spelling and accepting dotted or case-folded saved hints — the same
|
|
203
331
|
* normalization the inference request applies to the model id. Where several
|
|
204
332
|
* spellings match one id, the smallest wins: a ceiling stated twice is
|
|
@@ -207,9 +335,10 @@ const positiveTokenCount = (value: unknown): number | undefined =>
|
|
|
207
335
|
function devinModelTokenHint(
|
|
208
336
|
record: Record<string, number> | undefined,
|
|
209
337
|
modelUid: string,
|
|
338
|
+
familyBase?: string,
|
|
210
339
|
): number | undefined {
|
|
211
340
|
if (!record) return undefined;
|
|
212
|
-
for (const id of [modelUid, collapseDevinModelUid(modelUid)]) {
|
|
341
|
+
for (const id of [modelUid, ...(familyBase ? [familyBase] : []), collapseDevinModelUid(modelUid)]) {
|
|
213
342
|
const exact = Object.hasOwn(record, id) ? positiveTokenCount(record[id]) : undefined;
|
|
214
343
|
if (exact !== undefined) return exact;
|
|
215
344
|
const matches = Object.entries(record)
|
|
@@ -221,26 +350,6 @@ function devinModelTokenHint(
|
|
|
221
350
|
return undefined;
|
|
222
351
|
}
|
|
223
352
|
|
|
224
|
-
/**
|
|
225
|
-
* Resolve the INPUT ceiling for the exact UID selected for this turn. Catalog
|
|
226
|
-
* ClientModelConfig #18 and CompletionConfiguration #3 both carry input tokens;
|
|
227
|
-
* the independent output cap is not subtracted here. Smaller operator hints
|
|
228
|
-
* cap live evidence, never enlarge it. No evidence leaves the encoder's 128k
|
|
229
|
-
* fallback intact; an unrelated or opt-in long-context variant is not evidence.
|
|
230
|
-
*/
|
|
231
|
-
function resolveDevinMaxInputTokens(
|
|
232
|
-
provider: OcxProviderConfig,
|
|
233
|
-
modelUid: string,
|
|
234
|
-
liveWindow?: number,
|
|
235
|
-
): number | undefined {
|
|
236
|
-
const contextHint = devinModelTokenHint(provider.modelContextWindows, modelUid)
|
|
237
|
-
?? positiveTokenCount(provider.contextWindow);
|
|
238
|
-
const inputHint = devinModelTokenHint(provider.modelMaxInputTokens, modelUid);
|
|
239
|
-
const ceilings = [positiveTokenCount(liveWindow), contextHint, inputHint]
|
|
240
|
-
.filter((value): value is number => value !== undefined);
|
|
241
|
-
return ceilings.length > 0 ? Math.min(...ceilings) : undefined;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
353
|
/**
|
|
245
354
|
* Resolve the OUTPUT ceiling for this turn, highest authority first:
|
|
246
355
|
*
|
|
@@ -248,16 +357,17 @@ function resolveDevinMaxInputTokens(
|
|
|
248
357
|
* explicit cap is a request, so a small one is never widened into a
|
|
249
358
|
* configured larger one;
|
|
250
359
|
* 2. the configured per-model cap (`modelMaxOutputTokens`), read through the
|
|
251
|
-
*
|
|
360
|
+
* through the UID-aware hint lookup above;
|
|
252
361
|
* 3. the provider-wide `defaultMaxOutputTokens`;
|
|
253
|
-
* 4.
|
|
254
|
-
*
|
|
362
|
+
* 4. the catalog's own ceiling for the selected UID (ModelInfo #13) — without
|
|
363
|
+
* it every uncapped turn stopped at the encoder's 8192, far below the
|
|
364
|
+
* 128k most rows advertise;
|
|
365
|
+
* 5. undefined, which leaves the cloud-direct encoder's 8192 fallback in
|
|
366
|
+
* place when there is no catalog and nothing configured.
|
|
255
367
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* generate a whole window's worth of output. Nothing here reads
|
|
260
|
-
* `contextWindow` or `modelContextWindows` for that reason.
|
|
368
|
+
* A context window is not an output cap: feeding one into CompletionConfiguration
|
|
369
|
+
* #2 would ask Cognition to generate a whole window's worth of output. Nothing
|
|
370
|
+
* here reads `contextWindow` or `modelContextWindows` for that reason.
|
|
261
371
|
*
|
|
262
372
|
* Step 1 keeps the caller's raw value rather than `positiveTokenCount`: the
|
|
263
373
|
* inbound parser owns what a caller may send, and re-filtering here would
|
|
@@ -268,16 +378,37 @@ function resolveDevinMaxOutputTokens(
|
|
|
268
378
|
provider: OcxProviderConfig,
|
|
269
379
|
modelUid: string,
|
|
270
380
|
requested: number | undefined,
|
|
381
|
+
catalogRow?: Pick<ModelCatalogEntry, "maxOutputTokens" | "familyUid">,
|
|
271
382
|
): number | undefined {
|
|
272
383
|
if (typeof requested === "number") return requested;
|
|
273
|
-
|
|
274
|
-
|
|
384
|
+
const familyBase = catalogRow?.familyUid ? devinFamilyBaseId(catalogRow.familyUid) : undefined;
|
|
385
|
+
return devinModelTokenHint(provider.modelMaxOutputTokens, modelUid, familyBase)
|
|
386
|
+
?? positiveTokenCount(provider.defaultMaxOutputTokens)
|
|
387
|
+
?? positiveTokenCount(catalogRow?.maxOutputTokens);
|
|
275
388
|
}
|
|
276
389
|
|
|
277
|
-
/** Pure test
|
|
278
|
-
export const resolveDevinMaxInputTokensForTests = resolveDevinMaxInputTokens;
|
|
390
|
+
/** Pure test seam; runtime uses the same resolver immediately before dispatch. */
|
|
279
391
|
export const resolveDevinMaxOutputTokensForTests = resolveDevinMaxOutputTokens;
|
|
280
392
|
|
|
393
|
+
/** The classifier reads the selected UID's catalog input window, capped by configured limits. */
|
|
394
|
+
function resolveDevinContextWindow(
|
|
395
|
+
provider: OcxProviderConfig,
|
|
396
|
+
modelUid: string,
|
|
397
|
+
catalogRow?: Pick<ModelCatalogEntry, "contextWindow" | "familyUid">,
|
|
398
|
+
): number | undefined {
|
|
399
|
+
const familyBase = catalogRow?.familyUid ? devinFamilyBaseId(catalogRow.familyUid) : undefined;
|
|
400
|
+
const limits = [
|
|
401
|
+
positiveTokenCount(catalogRow?.contextWindow),
|
|
402
|
+
devinModelTokenHint(provider.modelContextWindows, modelUid, familyBase),
|
|
403
|
+
positiveTokenCount(provider.contextWindow),
|
|
404
|
+
devinModelTokenHint(provider.modelMaxInputTokens, modelUid, familyBase),
|
|
405
|
+
].filter((value): value is number => value !== undefined);
|
|
406
|
+
return limits.length > 0 ? Math.min(...limits) : undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Pure test seam for configured caps and selected-row lookup. */
|
|
410
|
+
export const resolveDevinContextWindowForTests = resolveDevinContextWindow;
|
|
411
|
+
|
|
281
412
|
export class DevinMissingCredentialError extends Error {
|
|
282
413
|
constructor() {
|
|
283
414
|
super("Devin live transport requires a Devin API key. Run ocx login devin to sign in with your Cognition/Devin account.");
|
|
@@ -364,45 +495,16 @@ function assistantText(message: OcxAssistantMessage): string {
|
|
|
364
495
|
// Thinking stays out of the replayed TEXT: folding chain-of-thought into
|
|
365
496
|
// assistant text sends it back as visible prior output, which the model
|
|
366
497
|
// then treats as something it said to the user. It is replayed in its own
|
|
367
|
-
// field instead — see
|
|
498
|
+
// field instead — see devinAssistantReasoning.
|
|
368
499
|
.map((part) => (part.type === "text" ? part.text : ""))
|
|
369
500
|
.filter(Boolean)
|
|
370
501
|
.join("\n");
|
|
371
502
|
}
|
|
372
503
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
* field and dropped the thinking outright, so a reasoning model restarted its
|
|
378
|
-
* chain on every turn of a tool loop. The field exists: two independent
|
|
379
|
-
* clients of the same service write #11 thinking with #12 signature and #18
|
|
380
|
-
* signature_type on the assistant prompt.
|
|
381
|
-
*
|
|
382
|
-
* Field #12 attests the exact text at #11, and the wire has room for one pair.
|
|
383
|
-
* Every block that carries text is replayed, so the chain stays intact; the
|
|
384
|
-
* signature rides along only when the text being replayed IS the text it
|
|
385
|
-
* attests, which is exactly the single-block case. Several independently signed
|
|
386
|
-
* blocks send an unsigned prompt rather than pairing one block's attestation
|
|
387
|
-
* with another block's words. A signature-only block attests encrypted thinking
|
|
388
|
-
* that is not being replayed at all, so it is not one of these blocks and
|
|
389
|
-
* cannot contribute the pair.
|
|
390
|
-
*/
|
|
391
|
-
function assistantThinking(
|
|
392
|
-
message: OcxAssistantMessage,
|
|
393
|
-
): { thinking?: string; signature?: string } {
|
|
394
|
-
const blocks = message.content.filter(
|
|
395
|
-
(part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
|
|
396
|
-
).filter(part => Boolean(part.thinking));
|
|
397
|
-
if (blocks.length === 0) return {};
|
|
398
|
-
const signature = blocks.length === 1 ? blocks[0]!.signature : undefined;
|
|
399
|
-
return {
|
|
400
|
-
thinking: blocks.map(part => part.thinking).join("\n"),
|
|
401
|
-
...(isProviderIssuedThinkingSignature(signature) ? { signature } : {}),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export function mapOcxMessagesToDevin(parsed: OcxParsedRequest): ChatHistoryItem[] {
|
|
504
|
+
export function mapOcxMessagesToDevin(
|
|
505
|
+
parsed: OcxParsedRequest,
|
|
506
|
+
options: { withholdAnthropicSignatures?: boolean } = {},
|
|
507
|
+
): ChatHistoryItem[] {
|
|
406
508
|
const items: ChatHistoryItem[] = [];
|
|
407
509
|
// Cognition is not an OpenAI host, and this adapter does advertise a real
|
|
408
510
|
// client tool catalog (proto #10 via `mapOcxToolsToDevin`), so the same
|
|
@@ -421,13 +523,17 @@ export function mapOcxMessagesToDevin(parsed: OcxParsedRequest): ChatHistoryItem
|
|
|
421
523
|
if (system) items.push({ role: "system", content: system });
|
|
422
524
|
|
|
423
525
|
for (const message of parsed.context.messages) {
|
|
424
|
-
const mapped = mapOneMessage(message);
|
|
526
|
+
const mapped = mapOneMessage(message, parsed.modelId, options);
|
|
425
527
|
if (mapped) items.push(mapped);
|
|
426
528
|
}
|
|
427
529
|
return items;
|
|
428
530
|
}
|
|
429
531
|
|
|
430
|
-
function mapOneMessage(
|
|
532
|
+
function mapOneMessage(
|
|
533
|
+
message: OcxMessage,
|
|
534
|
+
modelId: string,
|
|
535
|
+
options: { withholdAnthropicSignatures?: boolean },
|
|
536
|
+
): ChatHistoryItem | undefined {
|
|
431
537
|
if (message.role === "user" || message.role === "developer") {
|
|
432
538
|
const content = mapOcxContentToWire(message.content);
|
|
433
539
|
// An image with no caption text is a complete user message on its own.
|
|
@@ -439,10 +545,10 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
|
|
|
439
545
|
if (message.role === "assistant") {
|
|
440
546
|
const toolCalls = assistantToolCalls(message);
|
|
441
547
|
const text = assistantText(message);
|
|
442
|
-
const reasoning =
|
|
548
|
+
const reasoning = devinAssistantReasoning(message, modelId, options.withholdAnthropicSignatures === true);
|
|
443
549
|
// A turn that produced only reasoning is still worth replaying: dropping it
|
|
444
550
|
// is what makes the next turn re-derive the same chain.
|
|
445
|
-
if (!text && toolCalls.length === 0 && !reasoning.thinking) return undefined;
|
|
551
|
+
if (!text && toolCalls.length === 0 && !reasoning.thinking && !reasoning.signature) return undefined;
|
|
446
552
|
return {
|
|
447
553
|
role: "assistant",
|
|
448
554
|
content: text || "",
|
|
@@ -451,6 +557,10 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
|
|
|
451
557
|
};
|
|
452
558
|
}
|
|
453
559
|
if (message.role === "toolResult") {
|
|
560
|
+
// #9 alone is not enough: live, with a neutral "hello world" result flagged
|
|
561
|
+
// as an error, only gemini-3-8-flash reported a failure; swe-1-6,
|
|
562
|
+
// gpt-6-sol-low and gpt-5-6-luna-low read it as success. So the flag rides
|
|
563
|
+
// with the in-band marker rather than replacing it.
|
|
454
564
|
const wireContent = mapOcxContentToWire(message.content);
|
|
455
565
|
const toolContent = message.isError
|
|
456
566
|
? (typeof wireContent === "string"
|
|
@@ -461,6 +571,7 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
|
|
|
461
571
|
role: "tool",
|
|
462
572
|
content: toolContent,
|
|
463
573
|
tool_call_id: message.toolCallId,
|
|
574
|
+
...(message.isError ? { is_error: true } : {}),
|
|
464
575
|
};
|
|
465
576
|
}
|
|
466
577
|
return undefined;
|
|
@@ -613,8 +724,8 @@ export function createDevinAdapter(
|
|
|
613
724
|
// entry: an EU or FedStart account that used provider.baseUrl would send
|
|
614
725
|
// every RPC to the US server it is not provisioned on.
|
|
615
726
|
const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId, apiKey);
|
|
616
|
-
// One catalog read per turn serves model-UID resolution, the
|
|
617
|
-
//
|
|
727
|
+
// One catalog read per turn serves model-UID resolution, the output
|
|
728
|
+
// cap, and the chat pre-flight inside streamChatEvents. Failures are
|
|
618
729
|
// not cached, so a second read would only pay another fetch timeout on
|
|
619
730
|
// an otherwise valid turn.
|
|
620
731
|
const catalog = await getCachedCatalog(apiKey, host, incoming.abortSignal);
|
|
@@ -627,6 +738,11 @@ export function createDevinAdapter(
|
|
|
627
738
|
let openToolId: string | undefined;
|
|
628
739
|
let usage: OcxUsage | undefined;
|
|
629
740
|
let stopReason: string | undefined;
|
|
741
|
+
// Kept outside the try so the catch can tell an oversized history from a bad request.
|
|
742
|
+
let producedOutput = false;
|
|
743
|
+
let contextWindow: number | undefined;
|
|
744
|
+
let messages: ChatHistoryItem[] = [];
|
|
745
|
+
let tools: ToolDef[] | undefined;
|
|
630
746
|
|
|
631
747
|
const closeOpenTool = () => {
|
|
632
748
|
if (!openToolId) return;
|
|
@@ -636,11 +752,11 @@ export function createDevinAdapter(
|
|
|
636
752
|
|
|
637
753
|
try {
|
|
638
754
|
// Read the selected UID's catalog row, not the picker's collapsed base.
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
);
|
|
755
|
+
contextWindow = resolveDevinContextWindow(provider, modelUid, catalog?.byUid.get(modelUid));
|
|
756
|
+
messages = mapOcxMessagesToDevin(parsed);
|
|
757
|
+
tools = mapOcxToolsToDevin(parsed.context.tools);
|
|
642
758
|
const maxOutputTokens = resolveDevinMaxOutputTokens(
|
|
643
|
-
provider, modelUid, parsed.options.maxOutputTokens,
|
|
759
|
+
provider, modelUid, parsed.options.maxOutputTokens, catalog?.byUid.get(modelUid),
|
|
644
760
|
);
|
|
645
761
|
// A combo child has not committed an outer response yet. Holding its preflight through
|
|
646
762
|
// a reset wait would also hold the next-target fallback with no client keepalive.
|
|
@@ -648,18 +764,23 @@ export function createDevinAdapter(
|
|
|
648
764
|
// An admitted HTTP turn owns globally shared capacity until this call
|
|
649
765
|
// emits. Without an explicit wait allowance, preserve the typed reset
|
|
650
766
|
// delay in generated diagnostic wording and return immediately.
|
|
651
|
-
|
|
767
|
+
const signedMessages = mapOcxMessagesToDevin(parsed);
|
|
768
|
+
// A Claude signature is replayed because it is what carries the reasoning into this
|
|
769
|
+
// turn, but Cognition streams Claude's thinking as a summary the signature does not
|
|
770
|
+
// cover, and some replays are refused with invalid_argument before any output. That
|
|
771
|
+
// refusal is retried once with the Anthropic signatures withheld and the text kept.
|
|
772
|
+
const unsignedMessages = hasAnthropicSignature(signedMessages, parsed.modelId)
|
|
773
|
+
? mapOcxMessagesToDevin(parsed, { withholdAnthropicSignatures: true })
|
|
774
|
+
: undefined;
|
|
775
|
+
const request = (messages: ChatHistoryItem[]) => streamChatEventsWithResetRetry({
|
|
652
776
|
apiKey,
|
|
653
777
|
apiServerUrl: host,
|
|
654
778
|
modelUid,
|
|
655
779
|
catalog,
|
|
656
|
-
messages
|
|
657
|
-
tools
|
|
780
|
+
messages,
|
|
781
|
+
tools,
|
|
658
782
|
cascadeId,
|
|
659
|
-
// Input and output ceilings are separate wire fields. Omitting the
|
|
660
|
-
// input hint used to force every model through the 128k default.
|
|
661
783
|
completionOpts: {
|
|
662
|
-
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
|
|
663
784
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
664
785
|
...(typeof parsed.options.temperature === "number" ? { temperature: parsed.options.temperature } : {}),
|
|
665
786
|
...(typeof parsed.options.topP === "number" ? { topP: parsed.options.topP } : {}),
|
|
@@ -675,7 +796,80 @@ export function createDevinAdapter(
|
|
|
675
796
|
onPhysicalSend: incoming.onPhysicalSend,
|
|
676
797
|
onRecoveryWithheld: incoming.onRecoveryWithheld,
|
|
677
798
|
},
|
|
678
|
-
})
|
|
799
|
+
});
|
|
800
|
+
async function* withSignatureFallback() {
|
|
801
|
+
if (!unsignedMessages) {
|
|
802
|
+
yield* request(signedMessages);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Events from the signed attempt are held until its outcome is known: a refusal
|
|
806
|
+
// after reasoning would otherwise leave the client with the refused attempt's
|
|
807
|
+
// reasoning and signature, and the next turn would replay that signature against
|
|
808
|
+
// the retry's thinking.
|
|
809
|
+
const held: CloudChatEvent[] = [];
|
|
810
|
+
// Usage is still real: the refused attempt was processed, so its final counts are
|
|
811
|
+
// added to every usage frame of the retry (frames are cumulative per request).
|
|
812
|
+
let refusedUsage: OcxUsage | undefined;
|
|
813
|
+
let visible = false;
|
|
814
|
+
let heldPayloadBytes = 0;
|
|
815
|
+
// The iterator may pause before a trailer. A timer feeds the bridge during that
|
|
816
|
+
// pause without starting another upstream read or marking replay unsafe.
|
|
817
|
+
const heartbeatTimer = setInterval(() => {
|
|
818
|
+
if (!visible) emit({ type: "heartbeat" });
|
|
819
|
+
}, HELD_REASONING_HEARTBEAT_MS);
|
|
820
|
+
try {
|
|
821
|
+
for await (const event of request(signedMessages)) {
|
|
822
|
+
// Only visible output makes a retry unsafe. Live, the refusal often lands after the
|
|
823
|
+
// model has streamed its reasoning, its signature and a finish frame, and nothing else.
|
|
824
|
+
if (!visible && (event.kind === "text" || event.kind === "tool_call_start" || event.kind === "tool_call_args")) {
|
|
825
|
+
visible = true;
|
|
826
|
+
clearInterval(heartbeatTimer);
|
|
827
|
+
yield* held.splice(0);
|
|
828
|
+
}
|
|
829
|
+
if (visible) {
|
|
830
|
+
yield event;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
held.push(event);
|
|
834
|
+
if (event.kind === "usage") {
|
|
835
|
+
const next = toOcxDevinUsage(event);
|
|
836
|
+
refusedUsage = refusedUsage ? mergeDevinUsage(refusedUsage, next) : next;
|
|
837
|
+
}
|
|
838
|
+
if (event.kind === "reasoning") heldPayloadBytes += event.text.length * 2;
|
|
839
|
+
if (event.kind === "reasoning_signature") heldPayloadBytes += event.signature.length * 2;
|
|
840
|
+
if (held.length > HELD_REASONING_MAX_EVENTS || heldPayloadBytes > HELD_REASONING_MAX_PAYLOAD_BYTES) {
|
|
841
|
+
visible = true;
|
|
842
|
+
clearInterval(heartbeatTimer);
|
|
843
|
+
yield* held.splice(0);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
clearInterval(heartbeatTimer);
|
|
848
|
+
if (visible || !(error instanceof CloudChatError && error.code === "invalid_argument")) {
|
|
849
|
+
yield* held.splice(0);
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
// Emitted first so the counts survive a retry that reports no usage or fails early.
|
|
853
|
+
const cumulativeRefusedUsage = refusedUsage ? toCloudDevinUsage(refusedUsage) : undefined;
|
|
854
|
+
if (cumulativeRefusedUsage) yield cumulativeRefusedUsage;
|
|
855
|
+
try {
|
|
856
|
+
for await (const event of request(unsignedMessages)) {
|
|
857
|
+
yield event.kind === "usage" && cumulativeRefusedUsage ? addDevinUsage(event, cumulativeRefusedUsage) : event;
|
|
858
|
+
}
|
|
859
|
+
} catch (retryError) {
|
|
860
|
+
if (retryError instanceof SendBudgetExhaustedError) {
|
|
861
|
+
incoming.onRecoveryWithheld?.({ reason: "retry-send-budget" });
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
throw retryError;
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
} finally {
|
|
868
|
+
clearInterval(heartbeatTimer);
|
|
869
|
+
}
|
|
870
|
+
yield* held.splice(0);
|
|
871
|
+
}
|
|
872
|
+
for await (const event of withSignatureFallback()) {
|
|
679
873
|
if (incoming.abortSignal?.aborted) {
|
|
680
874
|
// Emitting nothing here left the bridge to synthesize adapter_eof.
|
|
681
875
|
// Say what happened instead, the way the other runTurn-only adapter
|
|
@@ -684,6 +878,7 @@ export function createDevinAdapter(
|
|
|
684
878
|
emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
|
|
685
879
|
return;
|
|
686
880
|
}
|
|
881
|
+
if (event.kind === "text" || event.kind === "reasoning" || event.kind === "tool_call_start") producedOutput = true;
|
|
687
882
|
if (event.kind === "text") {
|
|
688
883
|
closeOpenTool();
|
|
689
884
|
if (event.text) emit({ type: "text_delta", text: event.text });
|
|
@@ -696,7 +891,7 @@ export function createDevinAdapter(
|
|
|
696
891
|
if (event.kind === "reasoning_signature") {
|
|
697
892
|
// Carried back out so the next turn can replay it in the prompt's
|
|
698
893
|
// signature field; an unsigned replay is what the service ignores.
|
|
699
|
-
emit({ type: "thinking_signature", signature: event.signature });
|
|
894
|
+
emit({ type: "thinking_signature", signature: encodeDevinSignature(event.signature, event.signatureType) });
|
|
700
895
|
continue;
|
|
701
896
|
}
|
|
702
897
|
if (event.kind === "tool_call_start") {
|
|
@@ -724,15 +919,7 @@ export function createDevinAdapter(
|
|
|
724
919
|
continue;
|
|
725
920
|
}
|
|
726
921
|
if (event.kind === "usage") {
|
|
727
|
-
const
|
|
728
|
-
const next: OcxUsage = {
|
|
729
|
-
inputTokens: event.promptTokens ?? 0,
|
|
730
|
-
outputTokens: event.completionTokens ?? 0,
|
|
731
|
-
...(total > 0 ? { totalTokens: total } : {}),
|
|
732
|
-
...(event.cachedInputTokens !== undefined ? { cachedInputTokens: event.cachedInputTokens } : {}),
|
|
733
|
-
...(event.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: event.cacheCreationInputTokens } : {}),
|
|
734
|
-
...(event.reasoningTokens !== undefined ? { reasoningOutputTokens: event.reasoningTokens } : {}),
|
|
735
|
-
};
|
|
922
|
+
const next = toOcxDevinUsage(event);
|
|
736
923
|
// Merge rather than replace. A turn can carry more than one usage
|
|
737
924
|
// frame, and the counters are cumulative, so a later partial frame
|
|
738
925
|
// that omits a field used to zero a count the earlier frame had
|
|
@@ -756,6 +943,12 @@ export function createDevinAdapter(
|
|
|
756
943
|
// The Responses boundary already maps this local refusal to its structured 429 code.
|
|
757
944
|
// Converting it to an adapter event would make it an ordinary untyped upstream error.
|
|
758
945
|
if (error instanceof SendBudgetExhaustedError) throw error;
|
|
946
|
+
if (error instanceof CloudChatError && isDevinHistoryOverflow({
|
|
947
|
+
code: error.code, producedOutput, contextWindow, messages, tools,
|
|
948
|
+
})) {
|
|
949
|
+
emit({ ...devinContextOverflowEvent(), ...(usage ? { usage } : {}) });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
759
952
|
const message = error instanceof CloudChatError
|
|
760
953
|
? ("Devin cloud error" + (error.code ? " " + error.code : "") + ": " + error.message)
|
|
761
954
|
: error instanceof Error ? error.message : String(error);
|