@oh-my-pi/pi-ai 17.0.5 → 17.0.7
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/CHANGELOG.md +9 -0
- package/dist/types/auth-storage.d.ts +5 -5
- package/dist/types/registry/oauth/types.d.ts +3 -2
- package/package.json +4 -4
- package/src/auth-storage.ts +44 -36
- package/src/providers/devin.ts +95 -14
- package/src/registry/oauth/openai-codex.ts +22 -2
- package/src/registry/oauth/types.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.6] - 2026-07-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed OpenAI Codex credentials limited to one ChatGPT workspace per email: a personal Plus/Pro plan and a Team/Enterprise seat under the same email now coexist in the auth store — with separate rotation and usage pools — instead of the second login silently replacing the first. The workspace (`chatgpt_account_id`) is captured as the credential's org at login with the plan type as its display label, and two members of one workspace keep separate rows ([#2966](https://github.com/can1357/oh-my-pi/issues/2966)).
|
|
10
|
+
- Fixed Devin total-token usage omitting cache reads and cache writes.
|
|
11
|
+
- Fixed model switches to Devin rejecting foreign provider response IDs, reasoning signatures, and empty interrupted turns as invalid Cascade history.
|
|
12
|
+
- Classified zero-output Devin `invalid_argument` trailers as context overflow when the serialized message history is already large, routing cumulative tool-output payload failures through context maintenance—including artifact-backed shake rescue—instead of retrying the same rejected history.
|
|
13
|
+
|
|
5
14
|
## [17.0.5] - 2026-07-18
|
|
6
15
|
|
|
7
16
|
### Changed
|
|
@@ -99,7 +99,7 @@ export interface CredentialHealthResult {
|
|
|
99
99
|
email?: string;
|
|
100
100
|
/** OAuth account id if known. */
|
|
101
101
|
accountId?: string;
|
|
102
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
102
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
103
103
|
orgId?: string;
|
|
104
104
|
orgName?: string;
|
|
105
105
|
/** `true` when the refresh token lives on a remote broker (sentinel was present). */
|
|
@@ -490,7 +490,7 @@ export interface OAuthAccess {
|
|
|
490
490
|
projectId?: string;
|
|
491
491
|
enterpriseUrl?: string;
|
|
492
492
|
apiEndpoint?: string;
|
|
493
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
493
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
494
494
|
orgId?: string;
|
|
495
495
|
orgName?: string;
|
|
496
496
|
}
|
|
@@ -513,7 +513,7 @@ export interface OAuthAccessFailure {
|
|
|
513
513
|
projectId?: string;
|
|
514
514
|
enterpriseUrl?: string;
|
|
515
515
|
apiEndpoint?: string;
|
|
516
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
516
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
517
517
|
orgId?: string;
|
|
518
518
|
orgName?: string;
|
|
519
519
|
error: string;
|
|
@@ -528,7 +528,7 @@ export interface OAuthAccountIdentity {
|
|
|
528
528
|
accountId?: string;
|
|
529
529
|
email?: string;
|
|
530
530
|
projectId?: string;
|
|
531
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
531
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
532
532
|
orgId?: string;
|
|
533
533
|
orgName?: string;
|
|
534
534
|
}
|
|
@@ -549,7 +549,7 @@ export interface OAuthAccountSummary {
|
|
|
549
549
|
email?: string;
|
|
550
550
|
projectId?: string;
|
|
551
551
|
enterpriseUrl?: string;
|
|
552
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
552
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
553
553
|
orgId?: string;
|
|
554
554
|
orgName?: string;
|
|
555
555
|
}
|
|
@@ -11,8 +11,9 @@ export type OAuthCredentials = {
|
|
|
11
11
|
apiEndpoint?: string;
|
|
12
12
|
/**
|
|
13
13
|
* Organization/workspace the token is scoped to (e.g. an Anthropic org
|
|
14
|
-
* UUID). Captured once at login; token refreshes
|
|
15
|
-
* one account email hold credentials for multiple
|
|
14
|
+
* UUID or a ChatGPT workspace id). Captured once at login; token refreshes
|
|
15
|
+
* never rewrite it. Lets one account email hold credentials for multiple
|
|
16
|
+
* subscriptions.
|
|
16
17
|
*/
|
|
17
18
|
orgId?: string;
|
|
18
19
|
/** Human-readable organization name for display (may embed the email). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.7",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "17.0.7",
|
|
42
|
+
"@oh-my-pi/pi-utils": "17.0.7",
|
|
43
|
+
"@oh-my-pi/pi-wire": "17.0.7",
|
|
44
44
|
"arktype": "2.2.3",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/auth-storage.ts
CHANGED
|
@@ -186,7 +186,7 @@ export interface CredentialHealthResult {
|
|
|
186
186
|
email?: string;
|
|
187
187
|
/** OAuth account id if known. */
|
|
188
188
|
accountId?: string;
|
|
189
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
189
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
190
190
|
orgId?: string;
|
|
191
191
|
orgName?: string;
|
|
192
192
|
/** `true` when the refresh token lives on a remote broker (sentinel was present). */
|
|
@@ -735,7 +735,7 @@ export interface OAuthAccess {
|
|
|
735
735
|
projectId?: string;
|
|
736
736
|
enterpriseUrl?: string;
|
|
737
737
|
apiEndpoint?: string;
|
|
738
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
738
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
739
739
|
orgId?: string;
|
|
740
740
|
orgName?: string;
|
|
741
741
|
}
|
|
@@ -760,7 +760,7 @@ export interface OAuthAccessFailure {
|
|
|
760
760
|
projectId?: string;
|
|
761
761
|
enterpriseUrl?: string;
|
|
762
762
|
apiEndpoint?: string;
|
|
763
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
763
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
764
764
|
orgId?: string;
|
|
765
765
|
orgName?: string;
|
|
766
766
|
error: string;
|
|
@@ -776,7 +776,7 @@ export interface OAuthAccountIdentity {
|
|
|
776
776
|
accountId?: string;
|
|
777
777
|
email?: string;
|
|
778
778
|
projectId?: string;
|
|
779
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
779
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
780
780
|
orgId?: string;
|
|
781
781
|
orgName?: string;
|
|
782
782
|
}
|
|
@@ -795,7 +795,7 @@ export interface OAuthAccountSummary {
|
|
|
795
795
|
email?: string;
|
|
796
796
|
projectId?: string;
|
|
797
797
|
enterpriseUrl?: string;
|
|
798
|
-
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
798
|
+
/** Organization/workspace the credential is scoped to (Anthropic/ChatGPT multi-subscription). */
|
|
799
799
|
orgId?: string;
|
|
800
800
|
orgName?: string;
|
|
801
801
|
}
|
|
@@ -3283,15 +3283,14 @@ export class AuthStorage {
|
|
|
3283
3283
|
const identifiers: string[] = [];
|
|
3284
3284
|
const email = this.#getUsageReportMetadataValue(report, "email");
|
|
3285
3285
|
if (email) identifiers.push(`email:${email.toLowerCase()}`);
|
|
3286
|
-
if (report.provider === "anthropic") {
|
|
3287
|
-
//
|
|
3288
|
-
// (
|
|
3289
|
-
// merge — scope every identifier by org
|
|
3290
|
-
//
|
|
3291
|
-
//
|
|
3292
|
-
//
|
|
3293
|
-
//
|
|
3294
|
-
// and only merge among themselves.
|
|
3286
|
+
if (report.provider === "anthropic" || report.provider === "openai-codex") {
|
|
3287
|
+
// One account email can hold several org-scoped subscriptions
|
|
3288
|
+
// (Anthropic organizations, ChatGPT workspaces). Reports from
|
|
3289
|
+
// different orgs must not merge — scope every identifier by org
|
|
3290
|
+
// when the report carries one; fall back to the account when the
|
|
3291
|
+
// email could not be recovered so no-email reports still merge
|
|
3292
|
+
// per org. Org-less reports (pre-upgrade caches) keep their bare
|
|
3293
|
+
// identifiers and only merge among themselves.
|
|
3295
3294
|
if (identifiers.length === 0) {
|
|
3296
3295
|
const accountId =
|
|
3297
3296
|
this.#getUsageReportMetadataValue(report, "accountId") ?? this.#getUsageReportScopeAccountId(report);
|
|
@@ -3299,12 +3298,11 @@ export class AuthStorage {
|
|
|
3299
3298
|
}
|
|
3300
3299
|
const orgId = this.#getUsageReportMetadataValue(report, "orgId");
|
|
3301
3300
|
if (orgId) {
|
|
3302
|
-
if (identifiers.length === 0) return [
|
|
3303
|
-
return identifiers.map(
|
|
3301
|
+
if (identifiers.length === 0) return [`${report.provider}:org:${orgId.toLowerCase()}`];
|
|
3302
|
+
return identifiers.map(
|
|
3303
|
+
identifier => `${report.provider}:org:${orgId.toLowerCase()}|${identifier.toLowerCase()}`,
|
|
3304
|
+
);
|
|
3304
3305
|
}
|
|
3305
|
-
return identifiers.map(identifier => `anthropic:${identifier.toLowerCase()}`);
|
|
3306
|
-
}
|
|
3307
|
-
if (report.provider === "openai-codex") {
|
|
3308
3306
|
return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`);
|
|
3309
3307
|
}
|
|
3310
3308
|
const projectId =
|
|
@@ -5294,9 +5292,15 @@ export class AuthStorage {
|
|
|
5294
5292
|
if (credential.type !== "oauth") continue;
|
|
5295
5293
|
const credentialEmail = credential.email?.trim().toLowerCase();
|
|
5296
5294
|
const credentialAccountId = credential.accountId?.trim().toLowerCase();
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5295
|
+
// Every identity dimension present on BOTH sides must agree — the
|
|
5296
|
+
// account id is shared workspace-wide and one email can span
|
|
5297
|
+
// workspaces, so a single-dimension match can cross-link siblings.
|
|
5298
|
+
const emailComparable = Boolean(email && credentialEmail);
|
|
5299
|
+
const accountComparable = Boolean(accountId && credentialAccountId);
|
|
5300
|
+
if (!emailComparable && !accountComparable) continue;
|
|
5301
|
+
if (emailComparable && credentialEmail !== email) continue;
|
|
5302
|
+
if (accountComparable && credentialAccountId !== accountId) continue;
|
|
5303
|
+
matches.push(entry.id);
|
|
5300
5304
|
}
|
|
5301
5305
|
return matches;
|
|
5302
5306
|
}
|
|
@@ -5906,16 +5910,15 @@ function toStoredAuthCredential(row: AuthRow, credential: AuthCredential): Store
|
|
|
5906
5910
|
|
|
5907
5911
|
function resolveProviderCredentialIdentityKey(provider: string, identifiers: string[]): string | null {
|
|
5908
5912
|
const emailIdentifier = identifiers.find(identifier => identifier.startsWith("email:"));
|
|
5909
|
-
if (provider === "anthropic") {
|
|
5910
|
-
// One
|
|
5911
|
-
// Team seat plus a personal
|
|
5912
|
-
//
|
|
5913
|
-
//
|
|
5914
|
-
//
|
|
5915
|
-
//
|
|
5916
|
-
//
|
|
5917
|
-
//
|
|
5918
|
-
// org capture existed) keep their bare key.
|
|
5913
|
+
if (provider === "anthropic" || provider === "openai-codex") {
|
|
5914
|
+
// One account email can hold several organizations/workspaces (e.g. a
|
|
5915
|
+
// Team seat plus a personal plan), each with its own org-scoped token
|
|
5916
|
+
// and limit pools. Scope identity by org so both subscriptions can be
|
|
5917
|
+
// stored side by side. The qualifier rides on whichever base identity
|
|
5918
|
+
// is available, so an unqualified account/project fallback would
|
|
5919
|
+
// still collapse two subscriptions whenever the email could not be
|
|
5920
|
+
// recovered. Org-less credentials (rows written before org capture
|
|
5921
|
+
// existed) keep their bare key.
|
|
5919
5922
|
const base =
|
|
5920
5923
|
emailIdentifier ??
|
|
5921
5924
|
identifiers.find(identifier => identifier.startsWith("account:")) ??
|
|
@@ -5925,7 +5928,6 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
|
|
|
5925
5928
|
// No base identity at all: the org alone still distinguishes the row.
|
|
5926
5929
|
return orgIdentifier ?? null;
|
|
5927
5930
|
}
|
|
5928
|
-
if (provider === "openai-codex" && emailIdentifier) return emailIdentifier;
|
|
5929
5931
|
const accountIdentifier = identifiers.find(identifier => identifier.startsWith("account:"));
|
|
5930
5932
|
if (accountIdentifier) return accountIdentifier;
|
|
5931
5933
|
if (emailIdentifier) return emailIdentifier;
|
|
@@ -5962,9 +5964,9 @@ function matchesReplacementCredential(
|
|
|
5962
5964
|
if (incomingIdentityKey === existingIdentityKey) return true;
|
|
5963
5965
|
if (existingIdentityKey === null) return false;
|
|
5964
5966
|
// One-way upgrade, applied only when the INCOMING identity key carries the
|
|
5965
|
-
// org qualifier (only anthropic keys do, so other
|
|
5966
|
-
// checks below). An org-scoped login `org:<o>`
|
|
5967
|
-
// existing row that denotes the same subscription:
|
|
5967
|
+
// org qualifier (only anthropic and openai-codex keys do, so other
|
|
5968
|
+
// providers never reach the checks below). An org-scoped login `org:<o>`
|
|
5969
|
+
// claims (and re-keys) any existing row that denotes the same subscription:
|
|
5968
5970
|
// - `org:<o>` — org-only row stored when identity recovery failed, claimed
|
|
5969
5971
|
// once a later same-org login recovers a base identity;
|
|
5970
5972
|
// - `<b>` for any base identity `<b>` (email/account/project) the incoming
|
|
@@ -5988,10 +5990,16 @@ function matchesReplacementCredential(
|
|
|
5988
5990
|
existing.type === "oauth" && existingIdentityKey.endsWith(`|${orgIdentifier}`)
|
|
5989
5991
|
? extractOAuthCredentialIdentifiers(existing)
|
|
5990
5992
|
: null;
|
|
5993
|
+
// A base identifier that merely repeats the org qualifier's id carries no
|
|
5994
|
+
// per-user identity (openai-codex stores the ChatGPT workspace id as both
|
|
5995
|
+
// accountId and orgId, shared by every member) — letting it act as a
|
|
5996
|
+
// claimable base would re-key another member's same-org row.
|
|
5997
|
+
const orgQualifierId = orgIdentifier.slice("org:".length);
|
|
5991
5998
|
for (const identifier of incomingIdentifiers) {
|
|
5992
5999
|
const isBase =
|
|
5993
6000
|
identifier.startsWith("email:") || identifier.startsWith("account:") || identifier.startsWith("project:");
|
|
5994
6001
|
if (!isBase) continue;
|
|
6002
|
+
if (identifier.slice(identifier.indexOf(":") + 1) === orgQualifierId) continue;
|
|
5995
6003
|
if (existingIdentityKey === identifier) return true;
|
|
5996
6004
|
if (existingIdentityKey === `${identifier}|${orgIdentifier}`) return true;
|
|
5997
6005
|
if (existingIdentifiers?.includes(identifier)) return true;
|
package/src/providers/devin.ts
CHANGED
|
@@ -43,9 +43,11 @@ import type {
|
|
|
43
43
|
Tool,
|
|
44
44
|
ToolCall,
|
|
45
45
|
} from "../types";
|
|
46
|
+
import { isDemotedThinking } from "../utils/block-symbols";
|
|
46
47
|
import { deterministicUuid } from "../utils/deterministic-id";
|
|
47
48
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
48
49
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
50
|
+
import { transformMessages } from "./transform-messages";
|
|
49
51
|
|
|
50
52
|
/** Base host for Codeium/Windsurf's Cascade chat API (Connect protocol over HTTP/1.1). */
|
|
51
53
|
export const DEVIN_API_URL = "https://server.codeium.com";
|
|
@@ -78,6 +80,13 @@ const CONNECT_END_STREAM_FLAG = 0x02;
|
|
|
78
80
|
* fails fast instead of consuming memory.
|
|
79
81
|
*/
|
|
80
82
|
const MAX_CONNECT_FRAME_PAYLOAD = 16 * 1024 * 1024;
|
|
83
|
+
/**
|
|
84
|
+
* Recovery heuristic for opaque Devin `invalid_argument` trailers. This is not
|
|
85
|
+
* asserted to be the backend's hard limit: small requests can hit the same
|
|
86
|
+
* intermittent error, while compactable message history this large is likely
|
|
87
|
+
* to benefit from the existing context-overflow maintenance path.
|
|
88
|
+
*/
|
|
89
|
+
const LARGE_HISTORY_RECOVERY_BYTES = 512 * 1024;
|
|
81
90
|
|
|
82
91
|
export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
83
92
|
model: Model<"devin-agent">,
|
|
@@ -157,10 +166,14 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
157
166
|
const auth = await fetchDevinAuthMetadata(apiKey, baseUrl, fetchImpl, options?.signal);
|
|
158
167
|
const chatBaseUrl = auth.baseUrl ?? baseUrl;
|
|
159
168
|
const request = buildDevinChatRequest(model, context, options, apiKey, auth.userJwt);
|
|
160
|
-
logger.debug("devin: sending chat request", { model: model.id, tools: context.tools?.length ?? 0 });
|
|
161
|
-
|
|
162
169
|
const reqBytes = toBinary(GetChatMessageRequestSchema, request);
|
|
163
170
|
const gz = gzipSync(reqBytes);
|
|
171
|
+
logger.debug("devin: sending chat request", {
|
|
172
|
+
model: model.id,
|
|
173
|
+
tools: context.tools?.length ?? 0,
|
|
174
|
+
requestBytes: reqBytes.byteLength,
|
|
175
|
+
compressedBytes: gz.byteLength,
|
|
176
|
+
});
|
|
164
177
|
const frame = Buffer.alloc(5 + gz.length);
|
|
165
178
|
frame[0] = CONNECT_COMPRESSED_FLAG;
|
|
166
179
|
frame.writeUInt32BE(gz.length, 1);
|
|
@@ -223,7 +236,53 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
223
236
|
if (flag & CONNECT_END_STREAM_FLAG) {
|
|
224
237
|
const trailerBytes = flag & CONNECT_COMPRESSED_FLAG ? gunzipSync(payload) : payload;
|
|
225
238
|
const trailerError = readConnectTrailerError(trailerBytes.toString("utf8").trim());
|
|
226
|
-
if (trailerError)
|
|
239
|
+
if (trailerError) {
|
|
240
|
+
const error = new AIError.ValidationError(trailerError.formatted);
|
|
241
|
+
if (
|
|
242
|
+
firstTokenTime === undefined &&
|
|
243
|
+
trailerError.code.toLowerCase() === "invalid_argument" &&
|
|
244
|
+
/\binternal error\b/i.test(trailerError.message)
|
|
245
|
+
) {
|
|
246
|
+
// The full protobuf also contains the system prompt and tool
|
|
247
|
+
// schemas, which history maintenance cannot shrink. Re-encode
|
|
248
|
+
// only the repeated history field before choosing recovery.
|
|
249
|
+
let activeTailCount = 0;
|
|
250
|
+
const lastRole = context.messages.at(-1)?.role;
|
|
251
|
+
if (lastRole === "user" || lastRole === "developer") {
|
|
252
|
+
activeTailCount = 1;
|
|
253
|
+
// A trailing developer message can accompany the current user
|
|
254
|
+
// prompt. Earlier user-role records may instead be flushed
|
|
255
|
+
// execution history and must remain eligible for compaction.
|
|
256
|
+
if (lastRole === "developer") {
|
|
257
|
+
for (let i = context.messages.length - 2; i >= 0; i--) {
|
|
258
|
+
const role = context.messages[i].role;
|
|
259
|
+
if (role !== "user" && role !== "developer") break;
|
|
260
|
+
activeTailCount++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const shrinkablePrompts =
|
|
265
|
+
activeTailCount > 0
|
|
266
|
+
? request.chatMessagePrompts.slice(0, -activeTailCount)
|
|
267
|
+
: request.chatMessagePrompts;
|
|
268
|
+
const historyBytes = toBinary(
|
|
269
|
+
GetChatMessageRequestSchema,
|
|
270
|
+
create(GetChatMessageRequestSchema, {
|
|
271
|
+
chatMessagePrompts: shrinkablePrompts,
|
|
272
|
+
}),
|
|
273
|
+
).byteLength;
|
|
274
|
+
if (historyBytes >= LARGE_HISTORY_RECOVERY_BYTES) {
|
|
275
|
+
AIError.attach(error, AIError.create(AIError.Flag.ContextOverflow));
|
|
276
|
+
logger.warn("devin: treating large-history invalid_argument as context overflow", {
|
|
277
|
+
model: model.id,
|
|
278
|
+
historyBytes,
|
|
279
|
+
requestBytes: reqBytes.byteLength,
|
|
280
|
+
compressedBytes: gz.byteLength,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
227
286
|
continue;
|
|
228
287
|
}
|
|
229
288
|
|
|
@@ -322,7 +381,8 @@ export const streamDevin: StreamFunction<"devin-agent"> = (
|
|
|
322
381
|
output.usage.output = Number(msg.usage.outputTokens);
|
|
323
382
|
output.usage.cacheRead = Number(msg.usage.cacheReadTokens);
|
|
324
383
|
output.usage.cacheWrite = Number(msg.usage.cacheWriteTokens);
|
|
325
|
-
output.usage.totalTokens =
|
|
384
|
+
output.usage.totalTokens =
|
|
385
|
+
output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
326
386
|
}
|
|
327
387
|
}
|
|
328
388
|
|
|
@@ -442,6 +502,7 @@ function buildDevinChatRequest(
|
|
|
442
502
|
options?.stopSequences && options.stopSequences.length > 0
|
|
443
503
|
? [...DEVIN_DEFAULT_STOP_PATTERNS, ...options.stopSequences]
|
|
444
504
|
: DEVIN_DEFAULT_STOP_PATTERNS;
|
|
505
|
+
const messages = transformMessages(context.messages, model);
|
|
445
506
|
return create(GetChatMessageRequestSchema, {
|
|
446
507
|
metadata: create(MetadataSchema, {
|
|
447
508
|
apiKey,
|
|
@@ -453,7 +514,7 @@ function buildDevinChatRequest(
|
|
|
453
514
|
locale: "en",
|
|
454
515
|
}),
|
|
455
516
|
prompt: (context.systemPrompt ?? []).join("\n\n"),
|
|
456
|
-
chatMessagePrompts: buildChatMessagePrompts(
|
|
517
|
+
chatMessagePrompts: buildChatMessagePrompts(messages, cascadeId, model),
|
|
457
518
|
chatModelUid: options?.chatModelUid ?? model.requestModelId ?? model.id,
|
|
458
519
|
requestType: ChatMessageRequestType.CASCADE,
|
|
459
520
|
plannerMode: ConversationalPlannerMode.DEFAULT,
|
|
@@ -485,7 +546,11 @@ function buildDevinChatRequest(
|
|
|
485
546
|
}
|
|
486
547
|
|
|
487
548
|
/** Map omp `Message` history onto Cascade `ChatMessagePrompt`s (USER / SYSTEM / TOOL channels). */
|
|
488
|
-
function buildChatMessagePrompts(
|
|
549
|
+
function buildChatMessagePrompts(
|
|
550
|
+
messages: Message[],
|
|
551
|
+
cascadeId: string,
|
|
552
|
+
model: Model<"devin-agent">,
|
|
553
|
+
): ChatMessagePrompt[] {
|
|
489
554
|
const prompts: ChatMessagePrompt[] = [];
|
|
490
555
|
// messageId seeds are `cascadeId\0index\0role[...]` — prompt text is excluded
|
|
491
556
|
// so ids stay stable across content edits / history rebuilds.
|
|
@@ -513,16 +578,18 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
513
578
|
}),
|
|
514
579
|
);
|
|
515
580
|
} else if (msg.role === "assistant") {
|
|
581
|
+
const isNativeDevinMessage =
|
|
582
|
+
msg.api === model.api && msg.provider === model.provider && msg.model === model.id;
|
|
516
583
|
let promptText = "";
|
|
517
584
|
let thinkingText = "";
|
|
518
585
|
let signature = "";
|
|
519
586
|
const toolCalls: ChatToolCall[] = [];
|
|
520
587
|
for (const part of msg.content) {
|
|
521
588
|
if (part.type === "text") {
|
|
522
|
-
promptText += part.text
|
|
589
|
+
promptText += `${part.text}${isDemotedThinking(part) ? "\n" : ""}`;
|
|
523
590
|
} else if (part.type === "thinking") {
|
|
524
591
|
thinkingText += part.thinking;
|
|
525
|
-
if (!signature && part.thinkingSignature) signature = part.thinkingSignature;
|
|
592
|
+
if (isNativeDevinMessage && !signature && part.thinkingSignature) signature = part.thinkingSignature;
|
|
526
593
|
} else if (part.type === "toolCall") {
|
|
527
594
|
toolCalls.push(
|
|
528
595
|
create(ChatToolCallSchema, {
|
|
@@ -533,9 +600,13 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
533
600
|
);
|
|
534
601
|
}
|
|
535
602
|
}
|
|
603
|
+
if (!promptText && !thinkingText && !signature && toolCalls.length === 0) continue;
|
|
536
604
|
prompts.push(
|
|
537
605
|
create(ChatMessagePromptSchema, {
|
|
538
|
-
messageId:
|
|
606
|
+
messageId:
|
|
607
|
+
isNativeDevinMessage && msg.responseId
|
|
608
|
+
? msg.responseId
|
|
609
|
+
: `bot-${deterministicUuid(`${cascadeId}\0${index}\0assistant`)}`,
|
|
539
610
|
source: ChatMessageSource.SYSTEM,
|
|
540
611
|
prompt: promptText,
|
|
541
612
|
thinking: thinkingText,
|
|
@@ -569,12 +640,18 @@ function buildChatMessagePrompts(messages: Message[], cascadeId: string): ChatMe
|
|
|
569
640
|
return prompts;
|
|
570
641
|
}
|
|
571
642
|
|
|
643
|
+
interface ConnectTrailerError {
|
|
644
|
+
code: string;
|
|
645
|
+
message: string;
|
|
646
|
+
formatted: string;
|
|
647
|
+
}
|
|
648
|
+
|
|
572
649
|
/**
|
|
573
|
-
* Parse a Connect end-of-stream JSON trailer and return
|
|
574
|
-
*
|
|
575
|
-
*
|
|
650
|
+
* Parse a Connect end-of-stream JSON trailer and return its structured error
|
|
651
|
+
* when it carries `{ error: { code, message } }`, else `null`. The trailer is
|
|
652
|
+
* untrusted server output, so the shape is checked with guards rather than asserted.
|
|
576
653
|
*/
|
|
577
|
-
function readConnectTrailerError(text: string):
|
|
654
|
+
function readConnectTrailerError(text: string): ConnectTrailerError | null {
|
|
578
655
|
if (text.length === 0) return null;
|
|
579
656
|
let parsed: unknown;
|
|
580
657
|
try {
|
|
@@ -588,5 +665,9 @@ function readConnectTrailerError(text: string): string | null {
|
|
|
588
665
|
const code = "code" in err && typeof err.code === "string" ? err.code : "";
|
|
589
666
|
const message = "message" in err && typeof err.message === "string" ? err.message : "";
|
|
590
667
|
if (!code && !message) return null;
|
|
591
|
-
return
|
|
668
|
+
return {
|
|
669
|
+
code,
|
|
670
|
+
message,
|
|
671
|
+
formatted: `Devin stream error${code ? ` ${code}` : ""}: ${message}`,
|
|
672
|
+
};
|
|
592
673
|
}
|
|
@@ -31,6 +31,7 @@ const DEVICE_MAX_POLLS = 120;
|
|
|
31
31
|
type JwtPayload = {
|
|
32
32
|
[JWT_CLAIM_PATH]?: {
|
|
33
33
|
chatgpt_account_id?: string;
|
|
34
|
+
chatgpt_plan_type?: string;
|
|
34
35
|
};
|
|
35
36
|
[JWT_PROFILE_CLAIM]?: {
|
|
36
37
|
email?: string;
|
|
@@ -50,14 +51,27 @@ export function decodeJwt<T = Record<string, unknown>>(token: string): T | null
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Identity slice decoded from the token claims. The ChatGPT workspace
|
|
56
|
+
* (`chatgpt_account_id`) is the subscription pool the token draws limits
|
|
57
|
+
* from — one account email can hold several (e.g. a personal Pro plan plus a
|
|
58
|
+
* Team seat). `chatgpt_plan_type` may only be present on the `id_token`.
|
|
59
|
+
*/
|
|
60
|
+
function getTokenProfile(
|
|
61
|
+
accessToken: string,
|
|
62
|
+
idToken?: string,
|
|
63
|
+
): { accountId?: string; email?: string; planType?: string } {
|
|
54
64
|
const payload = decodeJwt<JwtPayload>(accessToken);
|
|
65
|
+
const idPayload = idToken ? decodeJwt<JwtPayload>(idToken) : null;
|
|
55
66
|
const auth = payload?.[JWT_CLAIM_PATH];
|
|
67
|
+
const idAuth = idPayload?.[JWT_CLAIM_PATH];
|
|
56
68
|
const accountId = auth?.chatgpt_account_id;
|
|
57
69
|
const email = payload?.[JWT_PROFILE_CLAIM]?.email?.trim().toLowerCase();
|
|
70
|
+
const planType = (auth?.chatgpt_plan_type ?? idAuth?.chatgpt_plan_type)?.trim().toLowerCase();
|
|
58
71
|
return {
|
|
59
72
|
accountId: typeof accountId === "string" && accountId.length > 0 ? accountId : undefined,
|
|
60
73
|
email: typeof email === "string" && email.length > 0 ? email : undefined,
|
|
74
|
+
planType: typeof planType === "string" && planType.length > 0 ? planType : undefined,
|
|
61
75
|
};
|
|
62
76
|
}
|
|
63
77
|
|
|
@@ -185,6 +199,7 @@ async function exchangeCodeForToken(
|
|
|
185
199
|
const tokenData = (await tokenResponse.json()) as {
|
|
186
200
|
access_token?: string;
|
|
187
201
|
refresh_token?: string;
|
|
202
|
+
id_token?: string;
|
|
188
203
|
expires_in?: number;
|
|
189
204
|
};
|
|
190
205
|
|
|
@@ -192,7 +207,7 @@ async function exchangeCodeForToken(
|
|
|
192
207
|
throw new AIError.OAuthError("Token response missing required fields", { kind: "validation" });
|
|
193
208
|
}
|
|
194
209
|
|
|
195
|
-
const { accountId, email } = getTokenProfile(tokenData.access_token);
|
|
210
|
+
const { accountId, email, planType } = getTokenProfile(tokenData.access_token, tokenData.id_token);
|
|
196
211
|
if (!accountId) {
|
|
197
212
|
throw new AIError.OAuthError("Failed to extract accountId from token", { kind: "validation" });
|
|
198
213
|
}
|
|
@@ -203,6 +218,8 @@ async function exchangeCodeForToken(
|
|
|
203
218
|
expires: Date.now() + tokenData.expires_in * 1000,
|
|
204
219
|
accountId,
|
|
205
220
|
email,
|
|
221
|
+
orgId: accountId,
|
|
222
|
+
orgName: planType,
|
|
206
223
|
};
|
|
207
224
|
}
|
|
208
225
|
|
|
@@ -354,6 +371,9 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAu
|
|
|
354
371
|
|
|
355
372
|
const { accountId, email } = getTokenProfile(tokenData.access_token);
|
|
356
373
|
|
|
374
|
+
// Deliberately no org fields on the result: the workspace a credential is
|
|
375
|
+
// scoped to is fixed at login. Callers merge refresh results over the
|
|
376
|
+
// stored credential, so omitting org here preserves it verbatim.
|
|
357
377
|
return {
|
|
358
378
|
access: tokenData.access_token,
|
|
359
379
|
refresh: tokenData.refresh_token || refreshToken,
|
|
@@ -12,8 +12,9 @@ export type OAuthCredentials = {
|
|
|
12
12
|
apiEndpoint?: string;
|
|
13
13
|
/**
|
|
14
14
|
* Organization/workspace the token is scoped to (e.g. an Anthropic org
|
|
15
|
-
* UUID). Captured once at login; token refreshes
|
|
16
|
-
* one account email hold credentials for multiple
|
|
15
|
+
* UUID or a ChatGPT workspace id). Captured once at login; token refreshes
|
|
16
|
+
* never rewrite it. Lets one account email hold credentials for multiple
|
|
17
|
+
* subscriptions.
|
|
17
18
|
*/
|
|
18
19
|
orgId?: string;
|
|
19
20
|
/** Human-readable organization name for display (may embed the email). */
|