@bitkyc08/opencodex 2.7.33 → 2.7.34

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 (64) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +21 -10
  4. package/README.ru.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/gui/dist/assets/index-BkmJJgg6.js +52 -0
  7. package/gui/dist/assets/index-Sg-7L_oZ.css +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic.ts +13 -6
  11. package/src/adapters/cursor/discovery.ts +39 -4
  12. package/src/adapters/cursor/exec-policy.ts +11 -13
  13. package/src/adapters/cursor/live-transport.ts +22 -4
  14. package/src/adapters/cursor/protobuf-events.ts +140 -8
  15. package/src/adapters/cursor/protobuf-request.ts +15 -0
  16. package/src/adapters/cursor/request-builder.ts +10 -5
  17. package/src/adapters/cursor/transport.ts +3 -2
  18. package/src/adapters/cursor/types.ts +14 -0
  19. package/src/adapters/kiro-constants.ts +12 -0
  20. package/src/adapters/kiro-errors.ts +111 -2
  21. package/src/adapters/kiro-events.ts +154 -35
  22. package/src/adapters/kiro-retry.ts +116 -32
  23. package/src/adapters/kiro-tools.ts +30 -20
  24. package/src/adapters/kiro-wire.ts +47 -6
  25. package/src/adapters/kiro.ts +891 -228
  26. package/src/adapters/openai-chat.ts +12 -5
  27. package/src/adapters/openai-responses.ts +7 -2
  28. package/src/bridge.ts +109 -26
  29. package/src/claude/outbound.ts +27 -4
  30. package/src/cli/index.ts +1 -1
  31. package/src/codex/catalog.ts +375 -33
  32. package/src/combos/index.ts +3 -0
  33. package/src/combos/request.ts +4 -4
  34. package/src/combos/resolve.ts +2 -2
  35. package/src/combos/types.ts +104 -2
  36. package/src/config.ts +70 -1
  37. package/src/lib/eventstream-decoder.ts +9 -0
  38. package/src/oauth/index.ts +3 -1
  39. package/src/oauth/kiro-credentials.ts +48 -20
  40. package/src/oauth/login-cli.ts +2 -0
  41. package/src/providers/derive.ts +8 -0
  42. package/src/providers/kiro-models.ts +2 -2
  43. package/src/providers/openai-sidecar.ts +28 -1
  44. package/src/providers/registry.ts +39 -2
  45. package/src/responses/parser.ts +22 -10
  46. package/src/responses/schema.ts +1 -0
  47. package/src/responses/state.ts +50 -10
  48. package/src/router.ts +15 -3
  49. package/src/server/auth-cors.ts +7 -0
  50. package/src/server/claude-messages.ts +6 -0
  51. package/src/server/index.ts +6 -3
  52. package/src/server/management-api.ts +187 -43
  53. package/src/server/ports.ts +4 -2
  54. package/src/server/request-log.ts +3 -2
  55. package/src/server/responses-item-id-repair.ts +281 -0
  56. package/src/server/responses.ts +274 -73
  57. package/src/types.ts +109 -16
  58. package/src/update/job.ts +81 -1
  59. package/src/vision/describe.ts +2 -1
  60. package/src/web-search/executor.ts +2 -1
  61. package/src/web-search/loop.ts +9 -1
  62. package/src/web-search/progress-stream.ts +12 -10
  63. package/gui/dist/assets/index-D6Fcl4yM.css +0 -1
  64. package/gui/dist/assets/index-d63HMU0x.js +0 -52
@@ -1,7 +1,7 @@
1
1
  import type { ProviderAdapter } from "./base";
2
2
  import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
3
3
  import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
4
- import { mapReasoningEffort } from "../reasoning-effort";
4
+ import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
5
5
  import { redactSecretString } from "../lib/redact";
6
6
  import { contentPartsToText } from "./image";
7
7
  import { neutralizeIdentity } from "./identity";
@@ -460,9 +460,15 @@ function usageFromOpenAIChat(usage: Record<string, unknown> | undefined): OcxUsa
460
460
  };
461
461
  }
462
462
 
463
- function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string): number | undefined {
463
+ function resolveMaxTokens(provider: OcxProviderConfig, parsed: OcxParsedRequest): number | undefined {
464
+ return parsed.options.maxOutputTokens
465
+ ?? modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
466
+ ?? provider.defaultMaxOutputTokens;
467
+ }
468
+
469
+ function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string, maxOutputTokens?: number): number | undefined {
464
470
  if (parsed.options.reasoning === "minimal") return 0;
465
- const maxBudget = parsed.options.maxOutputTokens ?? 32768;
471
+ const maxBudget = maxOutputTokens ?? 32768;
466
472
  const fractions: Record<string, number> = {
467
473
  low: 0.20,
468
474
  medium: 0.50,
@@ -495,6 +501,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
495
501
  messages,
496
502
  stream: parsed.stream,
497
503
  };
504
+ const maxTokens = resolveMaxTokens(provider, parsed);
498
505
  const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
499
506
  if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
500
507
  if (tools) body.tools = tools;
@@ -503,7 +510,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
503
510
  ? (toolChoice === "none" ? "none" : "auto")
504
511
  : toolChoice;
505
512
  }
506
- if (parsed.options.maxOutputTokens !== undefined) body.max_tokens = parsed.options.maxOutputTokens;
513
+ if (maxTokens !== undefined) body.max_tokens = maxTokens;
507
514
  if (parsed.options.temperature !== undefined && !modelInList(provider.noTemperatureModels, parsed.modelId)) {
508
515
  body.temperature = parsed.options.temperature;
509
516
  }
@@ -514,7 +521,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
514
521
  const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
515
522
  if (reasoningEffort !== undefined) {
516
523
  if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
517
- const budget = thinkingBudgetForEffort(parsed, reasoningEffort);
524
+ const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
518
525
  if (budget !== undefined) body.thinking_budget = budget;
519
526
  } else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
520
527
  // Vendor thinking-toggle wire (MiMo v2.x, GLM 5/5.1): the mapped value is the toggle
@@ -440,8 +440,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
440
440
  headers["chatgpt-account-id"] = override.chatgptAccountId;
441
441
  }
442
442
  } else {
443
- const base = provider.baseUrl.replace(/\/v1\/?$/, "");
444
- url = `${base}/v1/responses`;
443
+ if (provider.responsesPath === undefined) {
444
+ const base = provider.baseUrl.replace(/\/v1\/?$/, "");
445
+ url = `${base}/v1/responses`;
446
+ } else {
447
+ const base = provider.baseUrl.replace(/\/$/, "");
448
+ url = `${base}${provider.responsesPath}`;
449
+ }
445
450
  if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
446
451
  if (provider.headers) Object.assign(headers, provider.headers);
447
452
  }
package/src/bridge.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AdapterEvent, OcxUsage } from "./types";
1
+ import type { AdapterEvent, OcxMessagePhase, OcxProviderContinuationState, OcxUsage } from "./types";
2
2
  import { adapterFailureFromMessage, classifyError, type OcxErrorPayload } from "./lib/errors";
3
3
  import { encodeCompactionSummary } from "./responses/compaction";
4
4
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
@@ -43,6 +43,18 @@ function responseError(status: number, type: string, message: string): OcxErrorP
43
43
  return classifyError(status, type, message);
44
44
  }
45
45
 
46
+ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>): { httpStatus: number; error: OcxErrorPayload } {
47
+ if (event.status === undefined && event.errorType === undefined && event.code === undefined) {
48
+ return adapterFailureFromMessage(event.message);
49
+ }
50
+ const fallback = adapterFailureFromMessage(event.message);
51
+ const httpStatus = event.status ?? fallback.httpStatus;
52
+ const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
53
+ if (event.errorType !== undefined) error.type = event.errorType;
54
+ if (event.code !== undefined) error.code = event.code;
55
+ return { httpStatus, error };
56
+ }
57
+
46
58
  export { adapterFailureFromMessage } from "./lib/errors";
47
59
 
48
60
  /**
@@ -85,7 +97,7 @@ export function bridgeToResponsesSSE(
85
97
  /** One-shot: first non-empty text/thinking/raw-reasoning delta observed (WP4 TTFT). */
86
98
  onFirstOutput?: () => void;
87
99
  onTerminal?: (status: ResponsesTerminalStatus) => void;
88
- onCompletedResponse?: (response: Record<string, unknown>) => void;
100
+ onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
89
101
  },
90
102
  ): ReadableStream<Uint8Array> {
91
103
  // Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
@@ -173,9 +185,10 @@ export function bridgeToResponsesSSE(
173
185
  let outputIndex = 0;
174
186
  const finishedItems: OutputItem[] = [];
175
187
 
176
- const responseSnapshot = (status: string, output: OutputItem[]) => ({
188
+ const responseSnapshot = (status: string, output: OutputItem[], endTurn?: boolean) => ({
177
189
  id: responseId, object: "response", created_at: createdAt,
178
190
  status, model: modelId, output, usage: null,
191
+ ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
179
192
  });
180
193
 
181
194
  emit("response.created", { response: responseSnapshot("in_progress", []) });
@@ -213,7 +226,7 @@ export function bridgeToResponsesSSE(
213
226
  try { controller.enqueue(heartbeatFrame); } catch { closed = true; }
214
227
  }, heartbeatMs);
215
228
 
216
- let currentMsg: { itemId: string; outputIndex: number; text: string } | null = null;
229
+ let currentMsg: { itemId: string; outputIndex: number; text: string; phase?: OcxMessagePhase } | null = null;
217
230
  let currentReasoning: { itemId: string; outputIndex: number; text: string } | null = null;
218
231
  let currentRawReasoning: { itemId: string; outputIndex: number; text: string } | null = null;
219
232
  // Anthropic extended-thinking round-trip state: the signature signs the CURRENT thinking
@@ -299,6 +312,7 @@ export function bridgeToResponsesSSE(
299
312
  const item = {
300
313
  type: "message", id: currentMsg.itemId, status: "completed", role: "assistant",
301
314
  content: [{ type: "output_text", text: currentMsg.text, annotations }],
315
+ ...(currentMsg.phase ? { phase: currentMsg.phase } : {}),
302
316
  };
303
317
  emit("response.output_item.done", { output_index: currentMsg.outputIndex, item });
304
318
  finishedItems.push(item as OutputItem);
@@ -441,7 +455,7 @@ export function bridgeToResponsesSSE(
441
455
  // its compaction UI renders nothing mid-turn, so nothing is lost visually.
442
456
  if (options?.compaction) {
443
457
  if (event.type === "text_delta") { compactionText += event.text; continue; }
444
- if (event.type !== "done" && event.type !== "error") continue;
458
+ if (event.type !== "done" && event.type !== "incomplete" && event.type !== "error") continue;
445
459
  }
446
460
  switch (event.type) {
447
461
  case "text_delta": {
@@ -449,18 +463,20 @@ export function bridgeToResponsesSSE(
449
463
  if (currentRawReasoning) closeCurrentRawReasoning();
450
464
  flushHiddenRawReasoning();
451
465
  if (currentToolCall) closeCurrentToolCall();
466
+ if (currentMsg && currentMsg.phase !== event.phase) closeCurrentMessage();
452
467
  if (!currentMsg) {
453
468
  const itemId = `msg_${uuid()}`;
454
469
  const item = {
455
470
  type: "message", id: itemId, status: "in_progress", role: "assistant",
456
471
  content: [] as { type: string; text: string; annotations: never[] }[],
472
+ ...(event.phase ? { phase: event.phase } : {}),
457
473
  };
458
474
  emit("response.output_item.added", { output_index: outputIndex, item });
459
475
  emit("response.content_part.added", {
460
476
  item_id: itemId, output_index: outputIndex, content_index: 0,
461
477
  part: { type: "output_text", text: "", annotations: [] },
462
478
  });
463
- currentMsg = { itemId, outputIndex, text: "" };
479
+ currentMsg = { itemId, outputIndex, text: "", ...(event.phase ? { phase: event.phase } : {}) };
464
480
  }
465
481
  currentMsg.text += event.text;
466
482
  emit("response.output_text.delta", {
@@ -634,17 +650,49 @@ export function bridgeToResponsesSSE(
634
650
  finishedItems.push(item as OutputItem);
635
651
  outputIndex++;
636
652
  }
637
- const truncated = event.stopReason === "max_tokens";
638
- const response = {
639
- ...responseSnapshot(truncated ? "incomplete" : "completed", finishedItems),
640
- usage: responsesUsage(event.usage),
641
- ...(truncated ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
642
- };
643
- options?.onCompletedResponse?.(response);
644
- emit(truncated ? "response.incomplete" : "response.completed", {
645
- response,
653
+ if (event.stopReason === "max_tokens") {
654
+ // Upstream hit its output token cap. Surface as incomplete so the client can
655
+ // distinguish a truncated turn from a genuinely finished one (issue #246).
656
+ const response = {
657
+ ...responseSnapshot("incomplete", finishedItems, event.endTurn),
658
+ usage: responsesUsage(event.usage),
659
+ incomplete_details: { reason: "max_output_tokens" },
660
+ };
661
+ // Still cache the partial output so previous_response_id replay works.
662
+ options?.onCompletedResponse?.(response, event.providerState);
663
+ emit("response.incomplete", { response });
664
+ reportTerminal("incomplete");
665
+ } else {
666
+ const response = { ...responseSnapshot("completed", finishedItems, event.endTurn), usage: responsesUsage(event.usage) };
667
+ options?.onCompletedResponse?.(response, event.providerState);
668
+ emit("response.completed", {
669
+ response,
670
+ });
671
+ reportTerminal("completed");
672
+ }
673
+ terminated = true;
674
+ break;
675
+ }
676
+ case "incomplete": {
677
+ if (currentMsg) closeCurrentMessage();
678
+ if (currentReasoning) closeCurrentReasoning();
679
+ if (currentRawReasoning) closeCurrentRawReasoning();
680
+ flushHiddenRawReasoning();
681
+ if (currentToolCall) closeCurrentToolCall();
682
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
683
+ flushHiddenReasoningEnvelope();
684
+ emit("response.incomplete", {
685
+ response: {
686
+ ...responseSnapshot("incomplete", finishedItems, event.endTurn),
687
+ usage: responsesUsage(event.usage),
688
+ incomplete_details: {
689
+ reason: event.reason,
690
+ ...(event.message ? { message: event.message } : {}),
691
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
692
+ },
693
+ },
646
694
  });
647
- reportTerminal(truncated ? "incomplete" : "completed");
695
+ reportTerminal("incomplete");
648
696
  terminated = true;
649
697
  break;
650
698
  }
@@ -655,7 +703,7 @@ export function bridgeToResponsesSSE(
655
703
  flushHiddenRawReasoning();
656
704
  if (currentToolCall) closeCurrentToolCall();
657
705
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
658
- const failure = adapterFailureFromMessage(event.message);
706
+ const failure = adapterFailureFromEvent(event);
659
707
  emit("response.failed", {
660
708
  response: {
661
709
  ...responseSnapshot("failed", finishedItems),
@@ -664,6 +712,7 @@ export function bridgeToResponsesSSE(
664
712
  ...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
665
713
  error: failure.error,
666
714
  last_error: failure.error,
715
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
667
716
  },
668
717
  });
669
718
  reportTerminal("failed");
@@ -736,16 +785,20 @@ export function buildResponseJSON(
736
785
  toolSearchToolNames?: Set<string>;
737
786
  /** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
738
787
  compaction?: boolean;
788
+ onProviderState?: (state: OcxProviderContinuationState) => void;
739
789
  },
740
790
  ): Record<string, unknown> {
741
791
  const responseId = `resp_${uuid()}`;
742
792
  const output: OutputItem[] = [];
743
793
  let usage: OcxUsage | undefined;
744
- let errorMessage: string | undefined;
794
+ let errorEvent: Extract<AdapterEvent, { type: "error" }> | undefined;
795
+ let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
796
+ let endTurn: boolean | undefined;
745
797
  let stopReason: string | undefined;
746
798
  let compactionText = "";
747
799
 
748
800
  let currentText = "";
801
+ let currentTextPhase: OcxMessagePhase | undefined;
749
802
  let currentSummaryReasoning = "";
750
803
  let currentRawReasoning = "";
751
804
  // Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
@@ -774,8 +827,10 @@ export function buildResponseJSON(
774
827
  output.push({
775
828
  type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
776
829
  content: [{ type: "output_text", text: currentText, annotations }],
830
+ ...(currentTextPhase ? { phase: currentTextPhase } : {}),
777
831
  });
778
832
  currentText = "";
833
+ currentTextPhase = undefined;
779
834
  };
780
835
  const flushSummaryReasoning = () => {
781
836
  if (!currentSummaryReasoning && !batchSignature && batchRedacted.length === 0) return;
@@ -847,13 +902,17 @@ export function buildResponseJSON(
847
902
  for (const e of events) {
848
903
  switch (e.type) {
849
904
  case "text_delta":
905
+ if (currentText && currentTextPhase !== e.phase) flushText();
850
906
  if (currentSummaryReasoning) flushSummaryReasoning();
851
907
  if (currentRawReasoning) flushRawReasoning();
852
908
  if (currentToolCallId) flushToolCall();
853
909
  // Compaction turns keep the summary out of normal message output (replay dedup — see
854
910
  // bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
855
911
  if (options?.compaction) compactionText += e.text;
856
- else currentText += e.text;
912
+ else {
913
+ currentTextPhase = e.phase;
914
+ currentText += e.text;
915
+ }
857
916
  break;
858
917
  case "thinking_delta":
859
918
  if (currentText) flushText();
@@ -913,11 +972,19 @@ export function buildResponseJSON(
913
972
  }
914
973
  break;
915
974
  case "error":
916
- errorMessage = e.message;
975
+ errorEvent = e;
976
+ usage = e.usage ?? usage;
977
+ break;
978
+ case "incomplete":
979
+ incompleteEvent = e;
980
+ endTurn = e.endTurn;
981
+ if (e.providerState) options?.onProviderState?.(e.providerState);
917
982
  break;
918
983
  case "done":
919
984
  usage = e.usage;
920
- stopReason = e.stopReason;
985
+ endTurn = e.endTurn;
986
+ if (e.providerState) options?.onProviderState?.(e.providerState);
987
+ if (e.stopReason === "max_tokens") stopReason = "max_tokens";
921
988
  break;
922
989
  }
923
990
  }
@@ -925,18 +992,34 @@ export function buildResponseJSON(
925
992
  flushSummaryReasoning();
926
993
  flushRawReasoning();
927
994
  flushToolCall();
928
- if (options?.compaction && !errorMessage) {
995
+ if (options?.compaction && !errorEvent) {
929
996
  output.push({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) });
930
997
  }
931
998
 
999
+ const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
1000
+ const status = errorEvent
1001
+ ? "failed"
1002
+ : incompleteEvent || stopReason === "max_tokens"
1003
+ ? "incomplete"
1004
+ : "completed";
932
1005
  return {
933
1006
  id: responseId, object: "response",
934
1007
  created_at: Math.floor(Date.now() / 1000),
935
- status: errorMessage ? "failed" : stopReason === "max_tokens" ? "incomplete" : "completed",
1008
+ status,
936
1009
  model: modelId, output,
937
- ...(errorMessage ? { error: { message: errorMessage } } : {}),
938
- ...(!errorMessage && stopReason === "max_tokens" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
939
- usage: responsesUsage(usage),
1010
+ ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
1011
+ ...(failure ? { error: failure.error, last_error: failure.error } : {}),
1012
+ ...(errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
1013
+ ...(incompleteEvent ? {
1014
+ incomplete_details: {
1015
+ reason: incompleteEvent.reason,
1016
+ ...(incompleteEvent.message ? { message: incompleteEvent.message } : {}),
1017
+ ...(incompleteEvent.retryable !== undefined ? { retryable: incompleteEvent.retryable } : {}),
1018
+ },
1019
+ } : stopReason === "max_tokens" ? {
1020
+ incomplete_details: { reason: "max_output_tokens" },
1021
+ } : {}),
1022
+ usage: responsesUsage(incompleteEvent?.usage ?? usage),
940
1023
  };
941
1024
  }
942
1025
 
@@ -314,16 +314,26 @@ export function responsesSseToAnthropicSse(
314
314
  }
315
315
  case "response.completed": {
316
316
  const response = isRec(data.response) ? data.response : {};
317
+ if (response.end_turn === false && !sawToolUse) {
318
+ fail(529, "upstream turn ended without a final answer", true);
319
+ break;
320
+ }
317
321
  finish(sawToolUse ? "tool_use" : "end_turn", response.usage);
318
322
  break;
319
323
  }
320
324
  case "response.incomplete": {
321
325
  const response = isRec(data.response) ? data.response : {};
322
326
  const details = isRec(response.incomplete_details) ? response.incomplete_details : {};
323
- const reason = details.reason === "max_output_tokens" ? "max_tokens"
324
- : details.reason === "content_filter" ? "refusal"
325
- : sawToolUse ? "tool_use" : "end_turn";
326
- finish(reason, response.usage);
327
+ if (details.reason === "max_output_tokens") {
328
+ finish("max_tokens", response.usage);
329
+ } else if (details.reason === "content_filter") {
330
+ finish("refusal", response.usage);
331
+ } else {
332
+ const message = typeof details.message === "string" && details.message.trim()
333
+ ? details.message
334
+ : `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
335
+ fail(529, message, true);
336
+ }
327
337
  break;
328
338
  }
329
339
  case "response.failed": {
@@ -454,8 +464,21 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string): R
454
464
  }
455
465
 
456
466
  const details = isRec(body.incomplete_details) ? body.incomplete_details : {};
467
+ if (body.status === "incomplete"
468
+ && details.reason !== "max_output_tokens"
469
+ && details.reason !== "content_filter") {
470
+ const message = typeof details.message === "string" && details.message.trim()
471
+ ? details.message
472
+ : `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
473
+ return anthropicErrorBody(529, message, "overloaded_error");
474
+ }
475
+ if (body.status === "completed" && body.end_turn === false && !sawToolUse) {
476
+ return anthropicErrorBody(529, "upstream turn ended without a final answer", "overloaded_error");
477
+ }
457
478
  const stopReason = body.status === "incomplete" && details.reason === "max_output_tokens"
458
479
  ? "max_tokens"
480
+ : body.status === "incomplete" && details.reason === "content_filter"
481
+ ? "refusal"
459
482
  : sawToolUse ? "tool_use" : "end_turn";
460
483
 
461
484
  return {
package/src/cli/index.ts CHANGED
@@ -106,7 +106,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
106
106
  preferRetryIntervalMs: 50,
107
107
  allowEphemeralFallback: !hardPin,
108
108
  });
109
- if (selected !== preferred) {
109
+ if (preferred > 0 && selected !== preferred) {
110
110
  console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
111
111
  }
112
112
  if (shouldPersistSelectedPort(config.port, selected, preferred)) {