@gajae-code/ai 0.16.7 → 0.17.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/dist/types/auth-storage.d.ts +23 -4
  3. package/dist/types/models.d.ts +14 -0
  4. package/dist/types/provider-models/special.d.ts +12 -0
  5. package/dist/types/providers/anthropic.d.ts +1 -1
  6. package/dist/types/providers/cursor.d.ts +33 -21
  7. package/dist/types/providers/devin-acp.d.ts +157 -0
  8. package/dist/types/providers/google-gemini-headers.d.ts +1 -1
  9. package/dist/types/providers/mock.d.ts +2 -0
  10. package/dist/types/providers/openai-responses-shared.d.ts +21 -1
  11. package/dist/types/providers/register-builtins.d.ts +1 -0
  12. package/dist/types/types.d.ts +52 -11
  13. package/dist/types/utils/block-symbols.d.ts +15 -5
  14. package/dist/types/utils/fallback-transport.d.ts +4 -1
  15. package/dist/types/utils.d.ts +13 -0
  16. package/package.json +4 -3
  17. package/src/api-registry.ts +1 -0
  18. package/src/auth-broker/redact.ts +10 -2
  19. package/src/auth-gateway/server.ts +56 -3
  20. package/src/auth-storage.ts +330 -116
  21. package/src/model-manager.ts +21 -2
  22. package/src/models.d.ts +14 -0
  23. package/src/models.json +117 -0
  24. package/src/models.ts +18 -0
  25. package/src/provider-models/descriptors.ts +7 -0
  26. package/src/provider-models/openai-compat.ts +14 -0
  27. package/src/provider-models/special.ts +39 -0
  28. package/src/providers/anthropic.d.ts +1 -1
  29. package/src/providers/anthropic.ts +1 -1
  30. package/src/providers/azure-openai-responses.ts +10 -1
  31. package/src/providers/cursor.d.ts +33 -21
  32. package/src/providers/cursor.ts +2024 -508
  33. package/src/providers/devin-acp.d.ts +157 -0
  34. package/src/providers/devin-acp.ts +1103 -0
  35. package/src/providers/google-gemini-headers.d.ts +1 -1
  36. package/src/providers/google-gemini-headers.ts +1 -1
  37. package/src/providers/mock.ts +16 -1
  38. package/src/providers/openai-chat-server.ts +3 -3
  39. package/src/providers/openai-codex-responses.ts +27 -17
  40. package/src/providers/openai-responses-server.ts +5 -5
  41. package/src/providers/openai-responses-shared.d.ts +21 -1
  42. package/src/providers/openai-responses-shared.ts +60 -6
  43. package/src/providers/openai-responses.ts +10 -1
  44. package/src/providers/register-builtins.d.ts +1 -0
  45. package/src/providers/register-builtins.ts +21 -1
  46. package/src/stream.ts +14 -0
  47. package/src/types.d.ts +52 -11
  48. package/src/types.ts +70 -8
  49. package/src/utils/block-symbols.d.ts +15 -5
  50. package/src/utils/block-symbols.ts +16 -6
  51. package/src/utils/discovery/cursor.ts +3 -2
  52. package/src/utils/fallback-transport.d.ts +4 -1
  53. package/src/utils/fallback-transport.ts +12 -5
  54. package/src/utils.d.ts +13 -0
  55. package/src/utils.ts +17 -0
  56. package/dist/types/utils/codex-entitlement.d.ts +0 -22
  57. package/src/utils/codex-entitlement.d.ts +0 -22
  58. package/src/utils/codex-entitlement.ts +0 -57
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export declare const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export declare const DEFAULT_GEMINI_CLI_VERSION = "0.58.0";
8
+ export declare const DEFAULT_GEMINI_CLI_VERSION = "0.60.0";
9
9
  export declare function getGeminiCliUserAgent(modelId?: string): string;
10
10
  export declare const getGeminiCliHeaders: (modelId?: string) => {
11
11
  "User-Agent": string;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
7
7
  export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
- export const DEFAULT_GEMINI_CLI_VERSION = "0.58.0";
8
+ export const DEFAULT_GEMINI_CLI_VERSION = "0.60.0";
9
9
 
10
10
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
11
11
  const version =
@@ -98,6 +98,8 @@ export interface MockResponse {
98
98
  providerPayload?: AssistantMessage["providerPayload"];
99
99
  /** Optional typed provider failure metadata for retry/fallback tests. */
100
100
  transportFailure?: AssistantMessage["transportFailure"];
101
+ /** Bounded, redaction-safe failure classifier copied onto the terminal error message. */
102
+ errorCode?: string;
101
103
  /** If set, the stream emits a terminal error event instead of completing. */
102
104
  throw?: string | Error;
103
105
  /** Delay before any event is emitted. Honors the call's AbortSignal. */
@@ -363,7 +365,7 @@ async function runMock(
363
365
  : response.throw instanceof Error
364
366
  ? response.throw.message
365
367
  : String(response.throw);
366
- emitTerminalError(stream, model, startedAt, "error", message);
368
+ emitTerminalError(stream, model, startedAt, "error", message, response.errorCode ?? errorCodeOf(response.throw));
367
369
  return;
368
370
  }
369
371
 
@@ -485,6 +487,7 @@ function emitTerminalError(
485
487
  startedAt: number,
486
488
  reason: "aborted" | "error",
487
489
  message: string,
490
+ errorCode?: string,
488
491
  ): void {
489
492
  const failure: AssistantMessage = {
490
493
  role: "assistant",
@@ -495,6 +498,7 @@ function emitTerminalError(
495
498
  usage: emptyUsage(),
496
499
  stopReason: reason as StopReason,
497
500
  errorMessage: message,
501
+ ...(errorCode !== undefined ? { errorCode } : {}),
498
502
  timestamp: startedAt,
499
503
  duration: Date.now() - startedAt,
500
504
  };
@@ -502,6 +506,17 @@ function emitTerminalError(
502
506
  stream.push({ type: "error", reason, error: failure });
503
507
  }
504
508
 
509
+ /** Bounded safe-token classifier read off a thrown mock error, when present. */
510
+ function errorCodeOf(value: unknown): string | undefined {
511
+ try {
512
+ if (!(value instanceof Error)) return undefined;
513
+ const code = (value as { code?: unknown }).code;
514
+ return typeof code === "string" && code.length <= 64 && /^[A-Za-z0-9._-]+$/.test(code) ? code : undefined;
515
+ } catch {
516
+ return undefined;
517
+ }
518
+ }
519
+
505
520
  function sleep(ms: number, signal?: AbortSignal): Promise<void> {
506
521
  const { promise, resolve, reject } = Promise.withResolvers<void>();
507
522
  if (signal?.aborted) {
@@ -20,7 +20,7 @@ import type {
20
20
  ToolResultMessage,
21
21
  TSchema,
22
22
  } from "../types";
23
- import { sanitizeJsonStrings } from "../utils";
23
+ import { sanitizeJsonStrings, wireToolCallId } from "../utils";
24
24
  import {
25
25
  type OpenAIChatContentPart,
26
26
  type OpenAIChatMessage,
@@ -377,7 +377,7 @@ export function encodeResponse(message: AssistantMessage, requestedModelId: stri
377
377
  }
378
378
  if (toolCalls.length > 0) {
379
379
  responseMessage.tool_calls = toolCalls.map(tc => ({
380
- id: tc.id,
380
+ id: wireToolCallId(tc.id),
381
381
  type: "function",
382
382
  function: { name: tc.name, arguments: stringifyArgs(tc.arguments) },
383
383
  }));
@@ -639,7 +639,7 @@ export function encodeStream(
639
639
  tool_calls: [
640
640
  {
641
641
  index: idx,
642
- id: call?.id ?? "",
642
+ id: call ? wireToolCallId(call.id) : "",
643
643
  type: "function",
644
644
  function: { name: call?.name ?? "", arguments: "" },
645
645
  },
@@ -51,10 +51,6 @@ import {
51
51
  normalizeSystemPrompts,
52
52
  sanitizeOpenAIResponsesHistoryItemsForReplay,
53
53
  } from "../utils";
54
- import {
55
- formatOpenAICodexChatGPTEntitlementError,
56
- isOpenAICodexChatGPTEntitlementError,
57
- } from "../utils/codex-entitlement";
58
54
  import { AssistantMessageEventStream } from "../utils/event-stream";
59
55
  import { STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE, transportFailureFacts } from "../utils/fallback-transport";
60
56
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
@@ -134,7 +130,7 @@ const CODEX_PREVIOUS_RESPONSE_STALE_CODES = new Set(["previous_response_not_foun
134
130
  // on both sides of the anchor phrase), since a deterministic history fault such
135
131
  // as `Previous response's tool call ID is malformed.` must stay fatal:
136
132
  // replaying it re-sends the same offending item.
137
- const CODEX_PREVIOUS_RESPONSE_ID_TOKEN = String.raw`previous[ _-]response[ _-]id`;
133
+ const CODEX_PREVIOUS_RESPONSE_ID_TOKEN = `previous[ _-]response[ _-]id`;
138
134
  // Prose anchor reference ("Previous response with id 'resp_1' not found."):
139
135
  // the canonical `previous_response_not_found` wording, which codex-lb re-codes
140
136
  // as `invalid_request_error` the same way it re-codes the anchor-expiry fault
@@ -146,7 +142,7 @@ const CODEX_PREVIOUS_RESPONSE_PROSE_TOKEN = `previous[ _-]response`;
146
142
  // the same offending item. Tempering the qualifier⇄token scan against these
147
143
  // tokens keeps them fatal while pure anchor-stale prose still matches.
148
144
  const CODEX_PREVIOUS_RESPONSE_STALE_SUBFIELD_GUARD = `tool[ _]calls?|function[ _]calls?|custom[ _]tools?|call[ _-]?ids?|message[ _]?ids?|items?\\b|output[ _-]?items?`;
149
- const CODEX_ANCHOR_STALE_QUALIFIER = String.raw`invalid|expired|unknown|stale|not[ _-]?found|no longer`;
145
+ const CODEX_ANCHOR_STALE_QUALIFIER = `invalid|expired|unknown|stale|not[ _-]?found|no longer`;
150
146
  const CODEX_PREVIOUS_RESPONSE_STALE_MESSAGE = new RegExp(
151
147
  `(?:${CODEX_ANCHOR_STALE_QUALIFIER})[^\\n]{0,48}?${CODEX_PREVIOUS_RESPONSE_ID_TOKEN}` +
152
148
  `|${CODEX_PREVIOUS_RESPONSE_ID_TOKEN}[^\\n]{0,48}?(?:${CODEX_ANCHOR_STALE_QUALIFIER})`,
@@ -178,6 +174,7 @@ const CODEX_NON_RETRYABLE_EVENT_MESSAGE =
178
174
  /invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?|request blocked[^\n]*invalid[_ -]prompt|code=invalid[_ -]prompt/i;
179
175
  const CODEX_RETRYABLE_EVENT_MESSAGE =
180
176
  /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
177
+ const CODEX_ACCOUNT_MODEL_UNAVAILABLE_MESSAGE = /\bnot supported when using codex with a chatgpt account\b/i;
181
178
  const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
182
179
  const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
183
180
  const X_MODELS_ETAG_HEADER = "x-models-etag";
@@ -1246,7 +1243,7 @@ function handleCodexStreamEvent(args: {
1246
1243
  }
1247
1244
 
1248
1245
  if (eventType === "error" || eventType === "response.failed") {
1249
- throw createCodexProviderStreamError(rawEvent, model.id);
1246
+ throw createCodexProviderStreamError(rawEvent);
1250
1247
  }
1251
1248
 
1252
1249
  return firstTokenTime;
@@ -2865,14 +2862,13 @@ async function openCodexSseEventStream(
2865
2862
  updateCodexSessionMetadataFromHeaders(state, response.headers);
2866
2863
  if (!response.ok) {
2867
2864
  const info = await parseCodexError(response);
2868
- const error = new Error(
2869
- isOpenAICodexChatGPTEntitlementError(info.message, info.code)
2870
- ? formatOpenAICodexChatGPTEntitlementError(body.model)
2871
- : info.friendlyMessage || info.message,
2872
- );
2865
+ const error = new Error(info.friendlyMessage || info.message);
2873
2866
  (error as { headers?: Headers; status?: number }).headers = response.headers;
2874
2867
  (error as { headers?: Headers; status?: number }).status = response.status;
2875
2868
  (error as { code?: string }).code = info.code;
2869
+ if (isCodexAccountModelUnavailable(info.message, info.code)) {
2870
+ (error as { credentialModelUnavailable?: true }).credentialModelUnavailable = true;
2871
+ }
2876
2872
  throw error;
2877
2873
  }
2878
2874
  if (!response.body) {
@@ -3219,6 +3215,7 @@ function getCodexEventErrorMessage(rawEvent: Record<string, unknown>): string {
3219
3215
  class CodexProviderStreamError extends Error {
3220
3216
  readonly retryable: boolean;
3221
3217
  readonly code?: string;
3218
+ readonly credentialModelUnavailable: boolean;
3222
3219
  /**
3223
3220
  * Provider-supplied message, before display formatting appends `code=`/`status=`
3224
3221
  * metadata. Classification must read this, never `message`: the formatted string
@@ -3227,12 +3224,19 @@ class CodexProviderStreamError extends Error {
3227
3224
  */
3228
3225
  readonly providerMessage: string;
3229
3226
 
3230
- constructor(message: string, retryable: boolean, code: string | undefined, providerMessage: string) {
3227
+ constructor(
3228
+ message: string,
3229
+ retryable: boolean,
3230
+ code: string | undefined,
3231
+ providerMessage: string,
3232
+ credentialModelUnavailable: boolean,
3233
+ ) {
3231
3234
  super(message);
3232
3235
  this.name = "CodexProviderStreamError";
3233
3236
  this.retryable = retryable;
3234
3237
  this.code = code;
3235
3238
  this.providerMessage = providerMessage;
3239
+ this.credentialModelUnavailable = credentialModelUnavailable;
3236
3240
  }
3237
3241
  }
3238
3242
 
@@ -3251,12 +3255,11 @@ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolea
3251
3255
  return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
3252
3256
  }
3253
3257
 
3254
- function createCodexProviderStreamError(rawEvent: Record<string, unknown>, modelId: string): CodexProviderStreamError {
3258
+ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
3255
3259
  const code = getCodexEventErrorCode(rawEvent);
3256
3260
  const message = getCodexEventErrorMessage(rawEvent);
3257
- const formattedMessage = isOpenAICodexChatGPTEntitlementError(message, code)
3258
- ? formatOpenAICodexChatGPTEntitlementError(modelId)
3259
- : typeof rawEvent.type === "string" && rawEvent.type === "error"
3261
+ const formattedMessage =
3262
+ typeof rawEvent.type === "string" && rawEvent.type === "error"
3260
3263
  ? formatCodexErrorEvent(rawEvent, code, message)
3261
3264
  : (formatCodexFailure(rawEvent) ?? "Codex response failed");
3262
3265
  return new CodexProviderStreamError(
@@ -3264,6 +3267,13 @@ function createCodexProviderStreamError(rawEvent: Record<string, unknown>, model
3264
3267
  isRetryableCodexFailureEvent(rawEvent),
3265
3268
  code || undefined,
3266
3269
  message,
3270
+ isCodexAccountModelUnavailable(message, code),
3271
+ );
3272
+ }
3273
+
3274
+ function isCodexAccountModelUnavailable(message: string | undefined, code: string | undefined): boolean {
3275
+ return (
3276
+ CODEX_ACCOUNT_MODEL_UNAVAILABLE_MESSAGE.test(message ?? "") && code?.toLowerCase() === "invalid_request_error"
3267
3277
  );
3268
3278
  }
3269
3279
 
@@ -22,7 +22,7 @@ import type {
22
22
  Tool,
23
23
  ToolCall,
24
24
  } from "../types";
25
- import { sanitizeJsonStrings } from "../utils";
25
+ import { sanitizeJsonStrings, wireToolCallId } from "../utils";
26
26
  import {
27
27
  type OpenAIResponsesFunctionCallItem,
28
28
  type OpenAIResponsesFunctionCallOutputItem,
@@ -666,7 +666,7 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
666
666
  out.push({
667
667
  type: "custom_tool_call",
668
668
  id: part.thoughtSignature ?? makeCustomCallId(),
669
- call_id: part.id,
669
+ call_id: wireToolCallId(part.id),
670
670
  name: part.customWireName,
671
671
  input: rawInput,
672
672
  status: "completed",
@@ -675,7 +675,7 @@ function buildOutputItems(message: AssistantMessage): OutputItem[] {
675
675
  out.push({
676
676
  type: "function_call",
677
677
  id: part.thoughtSignature ?? makeFuncCallId(),
678
- call_id: part.id,
678
+ call_id: wireToolCallId(part.id),
679
679
  name: part.name,
680
680
  arguments: JSON.stringify(sanitizeJsonStrings(part.arguments ?? {})),
681
681
  status: "completed",
@@ -850,7 +850,7 @@ export function encodeStream(
850
850
  : undefined;
851
851
  const isCustom = customWireName !== undefined;
852
852
  const itemId = tc?.thoughtSignature ?? (isCustom ? makeCustomCallId() : makeFuncCallId());
853
- const callId = tc?.id ?? "";
853
+ const callId = tc ? wireToolCallId(tc.id) : "";
854
854
  const name = customWireName ?? tc?.name ?? "";
855
855
  const item = isCustom
856
856
  ? {
@@ -1143,7 +1143,7 @@ export function encodeStream(
1143
1143
  const tc = ev.toolCall;
1144
1144
  if (tc.customWireName && !cur.customWireName) cur.customWireName = tc.customWireName;
1145
1145
  if (tc.thoughtSignature) cur.itemId = tc.thoughtSignature;
1146
- cur.callId = tc.id;
1146
+ cur.callId = wireToolCallId(tc.id);
1147
1147
  cur.name = cur.customWireName ?? tc.name;
1148
1148
  if (cur.customWireName) {
1149
1149
  // Custom tool: raw input string. Streamed deltas accumulated
@@ -44,7 +44,27 @@ export interface ProcessResponsesStreamOptions {
44
44
  onFirstToken?: () => void;
45
45
  onOutputItemDone?: (item: ResponseOutputItem) => void;
46
46
  }
47
- export declare function processResponsesStream<TApi extends Api>(openaiStream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent>, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<TApi>, options?: ProcessResponsesStreamOptions): Promise<void>;
47
+ export declare function processResponsesStream<TApi extends Api>(openaiStream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent>, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<TApi>, options?: ProcessResponsesStreamOptions): Promise<boolean>;
48
+ /**
49
+ * Bounded classifier for a Responses stream that ended in failure before a
50
+ * successful terminal event: a `response.failed` envelope, a `response.completed`
51
+ * with `failed` status, a top-level `error` event, or an unexpected EOF (the
52
+ * stream returning without any terminal event). It is deliberately NOT a
53
+ * transport-retry fact: `transportFailureFacts` does not admit it, so preserving
54
+ * this diagnostic can never authorize a replay.
55
+ */
56
+ export declare const RESPONSES_STREAM_FAILURE_CODE = "upstream_stream_interrupted";
57
+ /**
58
+ * Typed error for an SSE stream that ended (EOF) before any terminal event, so
59
+ * the caller sees a bounded classifier instead of a content-free success.
60
+ */
61
+ export declare function unexpectedResponsesStreamEndError(): Error;
62
+ /**
63
+ * Read the bounded stream-failure classifier back off a thrown provider error.
64
+ * Only this module's own classifier is returned, so a foreign error's arbitrary
65
+ * `code` can never be forwarded onto the assistant message.
66
+ */
67
+ export declare function responsesStreamFailureCode(error: unknown): string | undefined;
48
68
  /**
49
69
  * Mark tool-call blocks left incomplete by a length-truncated response so the
50
70
  * agent loop rejects them instead of executing a best-effort partial parse.
@@ -52,6 +52,7 @@ const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES = new Set([
52
52
  "response.custom_tool_call_input.done",
53
53
  "response.output_item.done",
54
54
  "response.completed",
55
+ "response.incomplete",
55
56
  "response.failed",
56
57
  "error",
57
58
  ]);
@@ -447,7 +448,7 @@ export async function processResponsesStream<TApi extends Api>(
447
448
  stream: AssistantMessageEventStream,
448
449
  model: Model<TApi>,
449
450
  options?: ProcessResponsesStreamOptions,
450
- ): Promise<void> {
451
+ ): Promise<boolean> {
451
452
  type StreamItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
452
453
  type StreamBlock = ThinkingContent | TextContent | (ToolCall & { partialJson: string });
453
454
  interface ItemEntry {
@@ -592,6 +593,7 @@ export async function processResponsesStream<TApi extends Api>(
592
593
  };
593
594
  let sawFirstToken = false;
594
595
 
596
+ let sawTerminalEvent = false;
595
597
  for await (const event of openaiStream) {
596
598
  if (event.type === "response.created") {
597
599
  output.responseId = event.response.id;
@@ -963,14 +965,25 @@ export async function processResponsesStream<TApi extends Api>(
963
965
  dropEntry(item.id, event.output_index, item.call_id);
964
966
  stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
965
967
  }
966
- } else if (event.type === "response.completed") {
968
+ } else if (event.type === "response.completed" || event.type === "response.incomplete") {
969
+ sawTerminalEvent = true;
967
970
  const response = event.response;
968
971
  if (response?.id) {
969
972
  output.responseId = response.id;
970
973
  }
971
974
  populateResponsesUsageFromResponse(output, response?.usage);
972
975
  calculateCost(model, output.usage);
973
- output.stopReason = mapOpenAIResponsesStopReason(response?.status);
976
+ // A `response.incomplete` frame is terminal because the response was cut
977
+ // short, so the event type itself proves truncation. Deriving `length`
978
+ // from the event rather than trusting `response.status` stops a relay that
979
+ // drops or rewrites the status from turning a truncated turn into a plain
980
+ // `stop`/`toolUse` that would dispatch a tool call carrying repaired
981
+ // partial arguments with no `incompleteArguments` flag.
982
+ const terminalStatus = response?.status;
983
+ output.stopReason =
984
+ event.type === "response.incomplete" && terminalStatus !== "failed" && terminalStatus !== "cancelled"
985
+ ? "length"
986
+ : mapOpenAIResponsesStopReason(terminalStatus);
974
987
  if (response?.status === "failed" || response?.status === "cancelled") {
975
988
  const error = response?.error ?? (response as any)?.status_details?.error;
976
989
  const details = response?.incomplete_details;
@@ -998,8 +1011,9 @@ export async function processResponsesStream<TApi extends Api>(
998
1011
  output.stopReason = "toolUse";
999
1012
  }
1000
1013
  } else if (event.type === "error") {
1001
- throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
1014
+ throw createResponsesStreamFailureError(`Error Code ${event.code}: ${event.message}` || "Unknown error");
1002
1015
  } else if (event.type === "response.failed") {
1016
+ sawTerminalEvent = true;
1003
1017
  const error = event.response?.error ?? (event.response as any)?.status_details?.error;
1004
1018
  const details = event.response?.incomplete_details;
1005
1019
  const message = error
@@ -1010,6 +1024,45 @@ export async function processResponsesStream<TApi extends Api>(
1010
1024
  throw createResponsesFailedError(message, error?.code);
1011
1025
  }
1012
1026
  }
1027
+ return sawTerminalEvent;
1028
+ }
1029
+
1030
+ /**
1031
+ * Bounded classifier for a Responses stream that ended in failure before a
1032
+ * successful terminal event: a `response.failed` envelope, a `response.completed`
1033
+ * with `failed` status, a top-level `error` event, or an unexpected EOF (the
1034
+ * stream returning without any terminal event). It is deliberately NOT a
1035
+ * transport-retry fact: `transportFailureFacts` does not admit it, so preserving
1036
+ * this diagnostic can never authorize a replay.
1037
+ */
1038
+ export const RESPONSES_STREAM_FAILURE_CODE = "upstream_stream_interrupted";
1039
+
1040
+ /** Attach the bounded stream-failure classifier without touching retry facts. */
1041
+ function createResponsesStreamFailureError(message: string): Error {
1042
+ return Object.assign(new Error(message), { code: RESPONSES_STREAM_FAILURE_CODE });
1043
+ }
1044
+
1045
+ /**
1046
+ * Typed error for an SSE stream that ended (EOF) before any terminal event, so
1047
+ * the caller sees a bounded classifier instead of a content-free success.
1048
+ */
1049
+ export function unexpectedResponsesStreamEndError(): Error {
1050
+ return createResponsesStreamFailureError("Upstream response stream ended before a terminal response event");
1051
+ }
1052
+
1053
+ /**
1054
+ * Read the bounded stream-failure classifier back off a thrown provider error.
1055
+ * Only this module's own classifier is returned, so a foreign error's arbitrary
1056
+ * `code` can never be forwarded onto the assistant message.
1057
+ */
1058
+ export function responsesStreamFailureCode(error: unknown): string | undefined {
1059
+ try {
1060
+ return (error as { code?: unknown } | undefined)?.code === RESPONSES_STREAM_FAILURE_CODE
1061
+ ? RESPONSES_STREAM_FAILURE_CODE
1062
+ : undefined;
1063
+ } catch {
1064
+ return undefined;
1065
+ }
1013
1066
  }
1014
1067
 
1015
1068
  /**
@@ -1020,10 +1073,11 @@ export async function processResponsesStream<TApi extends Api>(
1020
1073
  * Exactly OpenAI's capacity-overload code is carried through as transport facts,
1021
1074
  * matched case-sensitively; every other failure stays a plain error, so an
1022
1075
  * untyped, cased, or malformed code can never reach a typed retry admission. The
1023
- * display message is unchanged either way.
1076
+ * display message is unchanged either way. Every non-overload envelope carries
1077
+ * the bounded stream-failure classifier instead of a typed retry fact.
1024
1078
  */
1025
1079
  function createResponsesFailedError(message: string, code: string | undefined): Error {
1026
- if (code !== SERVER_OVERLOADED_PROVIDER_CODE) return new Error(message);
1080
+ if (code !== SERVER_OVERLOADED_PROVIDER_CODE) return createResponsesStreamFailureError(message);
1027
1081
  const error = new Error(message) as Error & { openaiErrorCode?: string };
1028
1082
  error.openaiErrorCode = SERVER_OVERLOADED_PROVIDER_CODE;
1029
1083
  return error;
@@ -92,6 +92,8 @@ import {
92
92
  normalizeResponsesToolCallIdForTransform,
93
93
  processResponsesStream,
94
94
  repairOrphanResponsesToolOutputs,
95
+ responsesStreamFailureCode,
96
+ unexpectedResponsesStreamEndError,
95
97
  } from "./openai-responses-shared";
96
98
  import {
97
99
  applyOpenCodeGoSessionHeader,
@@ -468,7 +470,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
468
470
  stream.push({ type: "start", partial: output });
469
471
 
470
472
  const nativeOutputItems: Array<Record<string, unknown>> = [];
471
- await processResponsesStream(
473
+ const sawTerminalEvent = await processResponsesStream(
472
474
  iterateWithIdleTimeout(openaiStream, {
473
475
  idleTimeoutMs,
474
476
  firstItemTimeoutMs: firstEventTimeoutMs,
@@ -505,6 +507,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
505
507
  throw new Error(output.errorMessage ?? "An unknown error occurred");
506
508
  }
507
509
 
510
+ // The Responses SSE contract always ends with a terminal event. A stream
511
+ // that closes without one is an interrupted upstream, not a completed
512
+ // turn, and must not surface as a content-free success.
513
+ if (!sawTerminalEvent) throw unexpectedResponsesStreamEndError();
514
+
508
515
  output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, nativeOutputItems);
509
516
  if (isOpenCodeGoEmptyCompletedResponse(model, output, nativeOutputItems.length)) {
510
517
  output.stopReason = "error";
@@ -534,6 +541,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
534
541
  output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
535
542
  output.errorStatus = extractHttpStatusFromError(localAbortReason ?? normalizedError);
536
543
  output.transportFailure = transportFailureFacts(localAbortReason ?? normalizedError);
544
+ const streamFailureCode = responsesStreamFailureCode(localAbortReason ?? normalizedError);
545
+ if (streamFailureCode !== undefined) output.errorCode = streamFailureCode;
537
546
  output.errorMessage =
538
547
  localAbortReason?.message ?? (await finalizeErrorMessage(normalizedError, rawRequestDump));
539
548
  output.errorMessage = rewriteCopilotError(output.errorMessage, normalizedError, model.provider);
@@ -53,6 +53,7 @@ export declare const streamOpenAICodexResponses: (model: Model<"openai-codex-res
53
53
  export declare const streamOpenAICompletions: (model: Model<"openai-completions">, context: Context, options: OptionsForApi<"openai-completions">, onStreamCreated?: () => void) => EventStreamImpl;
54
54
  export declare const streamOpenAIResponses: (model: Model<"openai-responses">, context: Context, options: OptionsForApi<"openai-responses">, onStreamCreated?: () => void) => EventStreamImpl;
55
55
  export declare const streamCursor: (model: Model<"cursor-agent">, context: Context, options: OptionsForApi<"cursor-agent">, onStreamCreated?: () => void) => EventStreamImpl;
56
+ export declare const streamDevinAcp: (model: Model<"devin-acp">, context: Context, options: import("../types").StreamOptions, onStreamCreated?: () => void) => EventStreamImpl;
56
57
  export declare const streamOllama: (model: Model<"ollama-chat">, context: Context, options: OptionsForApi<"ollama-chat">, onStreamCreated?: () => void) => EventStreamImpl;
57
58
  export declare const streamBedrock: (model: Model<"bedrock-converse-stream">, context: Context, options: OptionsForApi<"bedrock-converse-stream">, onStreamCreated?: () => void) => EventStreamImpl;
58
59
  export declare const streamKiroCodeWhisperer: (model: Model<"kiro-codewhisperer-stream">, context: Context, options: OptionsForApi<"kiro-codewhisperer-stream">, onStreamCreated?: () => void) => EventStreamImpl;
@@ -34,6 +34,7 @@ import type { BedrockOptions } from "./amazon-bedrock";
34
34
  import type { AnthropicOptions } from "./anthropic";
35
35
  import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses";
36
36
  import type { CursorOptions } from "./cursor";
37
+ import type { DevinAcpOptions } from "./devin-acp";
37
38
  import type { GoogleOptions } from "./google";
38
39
  import type { GoogleGeminiCliOptions } from "./google-gemini-cli";
39
40
  import type { GoogleVertexOptions } from "./google-vertex";
@@ -146,6 +147,14 @@ interface CursorProviderModule {
146
147
  ) => AssistantMessageEventStream;
147
148
  }
148
149
 
150
+ interface DevinAcpProviderModule {
151
+ streamDevinAcp: (
152
+ model: Model<"devin-acp">,
153
+ context: Context,
154
+ options: DevinAcpOptions,
155
+ ) => AssistantMessageEventStream;
156
+ }
157
+
149
158
  interface BedrockProviderModule {
150
159
  streamBedrock: (
151
160
  model: Model<"bedrock-converse-stream">,
@@ -176,6 +185,7 @@ let openAICompletionsProviderModulePromise: Promise<LazyProviderModule<"openai-c
176
185
  let openAIResponsesProviderModulePromise: Promise<LazyProviderModule<"openai-responses">> | undefined;
177
186
  let ollamaProviderModulePromise: Promise<LazyProviderModule<"ollama-chat">> | undefined;
178
187
  let cursorProviderModulePromise: Promise<LazyProviderModule<"cursor-agent">> | undefined;
188
+ let devinAcpProviderModulePromise: Promise<LazyProviderModule<"devin-acp">> | undefined;
179
189
  let bedrockProviderModuleOverride: LazyProviderModule<"bedrock-converse-stream"> | undefined;
180
190
  let kiroCodeWhispererProviderModulePromise: Promise<LazyProviderModule<"kiro-codewhisperer-stream">> | undefined;
181
191
  let bedrockProviderModulePromise: Promise<LazyProviderModule<"bedrock-converse-stream">> | undefined;
@@ -459,6 +469,14 @@ function loadCursorProviderModule(): Promise<LazyProviderModule<"cursor-agent">>
459
469
  return cursorProviderModulePromise;
460
470
  }
461
471
 
472
+ function loadDevinAcpProviderModule(): Promise<LazyProviderModule<"devin-acp">> {
473
+ devinAcpProviderModulePromise ||= Promise.resolve().then(() => {
474
+ const provider = require("./devin-acp") as DevinAcpProviderModule;
475
+ return { stream: provider.streamDevinAcp };
476
+ });
477
+ return devinAcpProviderModulePromise;
478
+ }
479
+
462
480
  function loadBedrockProviderModule(): Promise<LazyProviderModule<"bedrock-converse-stream">> {
463
481
  if (bedrockProviderModuleOverride) {
464
482
  return Promise.resolve(bedrockProviderModuleOverride);
@@ -492,6 +510,7 @@ export const PROVIDER_RUNTIME_DESCRIPTORS: readonly ProviderRuntimeDescriptor<Ap
492
510
  { api: "openai-responses", load: loadOpenAIResponsesProviderModule },
493
511
  { api: "ollama-chat", load: loadOllamaProviderModule },
494
512
  { api: "cursor-agent", load: loadCursorProviderModule },
513
+ { api: "devin-acp", load: loadDevinAcpProviderModule },
495
514
  { api: "kiro-codewhisperer-stream", load: loadKiroCodeWhispererProviderModule },
496
515
  { api: "bedrock-converse-stream", load: loadBedrockProviderModule },
497
516
  ] as readonly ErasedProviderRuntimeDescriptor[];
@@ -537,7 +556,8 @@ export const streamOpenAIResponses = createLazyStream(
537
556
  loadOpenAIResponsesProviderModule,
538
557
  PROVIDER_OWNED_STREAM_WATCHDOG,
539
558
  );
540
- export const streamCursor = createLazyStream(loadCursorProviderModule);
559
+ export const streamCursor = createLazyStream(loadCursorProviderModule, PROVIDER_OWNED_STREAM_WATCHDOG);
560
+ export const streamDevinAcp = createLazyStream(loadDevinAcpProviderModule, PROVIDER_OWNED_STREAM_WATCHDOG);
541
561
  export const streamOllama = createLazyStream(loadOllamaProviderModule);
542
562
 
543
563
  export const streamBedrock = createLazyStream(loadBedrockProviderModule);
package/src/stream.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  readAwsStaticEnvironmentCredentials,
40
40
  } from "./providers/aws-credential-config";
41
41
  import type { CursorOptions } from "./providers/cursor";
42
+ import type { DevinAcpOptions } from "./providers/devin-acp";
42
43
  import type { GoogleOptions } from "./providers/google";
43
44
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
44
45
  import type { GoogleVertexOptions } from "./providers/google-vertex";
@@ -54,6 +55,7 @@ import {
54
55
  streamAzureOpenAIResponses,
55
56
  streamBedrock,
56
57
  streamCursor,
58
+ streamDevinAcp,
57
59
  streamGoogle,
58
60
  streamGoogleGeminiCli,
59
61
  streamGoogleVertex,
@@ -398,6 +400,9 @@ export function stream<TApi extends Api>(
398
400
  (options || {}) as KiroCodeWhispererOptions,
399
401
  onStreamCreated,
400
402
  );
403
+ } else if (model.api === "devin-acp") {
404
+ // Devin authenticates through its own CLI, so there is no GJC API key to resolve.
405
+ return streamDevinAcp(model as Model<"devin-acp">, context, (options || {}) as DevinAcpOptions, onStreamCreated);
401
406
  }
402
407
 
403
408
  const apiKey = options?.apiKey || (model.provider === "opencodex" ? "local" : getEnvApiKey(model.provider));
@@ -944,12 +949,15 @@ function mapOptionsForApi<TApi extends Api>(
944
949
  repetitionPenalty: options?.repetitionPenalty,
945
950
  maxTokens: resolveDefaultRequestMaxTokens(model, options?.maxTokens),
946
951
  signal: options?.signal,
952
+ streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
953
+ streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
947
954
  apiKey: apiKey || options?.apiKey,
948
955
  fallbackManaged: options?.fallbackManaged,
949
956
  fallbackAttempt: options?.fallbackAttempt,
950
957
  cacheRetention: options?.cacheRetention ?? model.cacheRetention,
951
958
  headers: options?.headers,
952
959
  initiatorOverride: options?.initiatorOverride,
960
+ maintenanceCall: options?.maintenanceCall,
953
961
  maxRetryDelayMs: options?.maxRetryDelayMs,
954
962
  requestMaxRetries: options?.fallbackManaged ? 0 : options?.requestMaxRetries,
955
963
  streamMaxRetries: options?.fallbackManaged ? 0 : options?.streamMaxRetries,
@@ -957,6 +965,7 @@ function mapOptionsForApi<TApi extends Api>(
957
965
  sessionId: options?.sessionId,
958
966
  providerSessionId: options?.providerSessionId,
959
967
  providerSessionState: options?.providerSessionState,
968
+ devinAcp: options?.devinAcp,
960
969
  onPayload: options?.onPayload,
961
970
  onResponse: options?.onResponse,
962
971
  onStreamCreated: options?.onStreamCreated,
@@ -1265,6 +1274,11 @@ function mapOptionsForApi<TApi extends Api>(
1265
1274
  });
1266
1275
  }
1267
1276
 
1277
+ case "devin-acp":
1278
+ return castApi<"devin-acp">({
1279
+ ...base,
1280
+ });
1281
+
1268
1282
  case "kiro-codewhisperer-stream":
1269
1283
  return castApi<"kiro-codewhisperer-stream">({
1270
1284
  ...base,