@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,5 +1,5 @@
1
- import type { OcxComboDefaultEffort, OcxComboTarget } from "../types";
2
- import { parseComboModelId } from "./types";
1
+ import type { OcxComboDefaultEffort, OcxComboTarget, OcxConfig } from "../types";
2
+ import { resolveComboId } from "./types";
3
3
 
4
4
  const warnedUnsupportedDefaults = new Set<string>();
5
5
 
@@ -7,11 +7,11 @@ export function resetComboEffortWarningStateForTests(): void {
7
7
  warnedUnsupportedDefaults.clear();
8
8
  }
9
9
 
10
- export function comboIdFromRawBody(body: unknown): string | null {
10
+ export function comboIdFromRawBody(body: unknown, config: OcxConfig): string | null {
11
11
  if (!body || typeof body !== "object" || Array.isArray(body)) return null;
12
12
  const model = (body as { model?: unknown }).model;
13
13
  if (typeof model !== "string") return null;
14
- return parseComboModelId(model);
14
+ return resolveComboId(config, model);
15
15
  }
16
16
 
17
17
  export function concreteComboRequestBody(
@@ -1,6 +1,6 @@
1
1
  import type { OcxComboTarget, OcxConfig } from "../types";
2
2
  import { coolComboTarget, isComboTargetInCooldown } from "./failover";
3
- import { getCombo, parseComboModelId, targetKey } from "./types";
3
+ import { getCombo, resolveComboId, targetKey } from "./types";
4
4
  import type { NormalizedComboConfig } from "./types";
5
5
 
6
6
  export interface ComboPick {
@@ -162,7 +162,7 @@ export function clearComboSelectionState(comboId?: string): void {
162
162
  }
163
163
 
164
164
  export function tryPickComboModel(config: OcxConfig, modelId: string): ComboPick | null {
165
- const comboId = parseComboModelId(modelId);
165
+ const comboId = resolveComboId(config, modelId);
166
166
  if (!comboId) return null;
167
167
  if (!getCombo(config, comboId)) throw new UnknownComboError(comboId);
168
168
  const picked = pickComboTarget(config, comboId);
@@ -9,6 +9,18 @@ import type {
9
9
 
10
10
  export const COMBO_NAMESPACE = "combo";
11
11
  const COMBO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
12
+ /**
13
+ * Public alias shape: one optional "/" segment, each segment id-shaped. Bare aliases
14
+ * (no "/") are the masquerade case — the combo answers to a mandated model id with no
15
+ * `combo/` prefix. Codex-facing slugs tolerate at most one "/", so deeper paths reject.
16
+ */
17
+ const COMBO_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
18
+ /**
19
+ * Bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are
20
+ * rejected: they collide with native catalog rows and the canonical-OpenAI routing
21
+ * branch, which cannot be shadowed honestly.
22
+ */
23
+ const NATIVE_OPENAI_FAMILY_PATTERN = /^(?:gpt-|o1-|o3-|o4-|codex-)/;
12
24
 
13
25
  export interface ComboValidationIssue {
14
26
  path: Array<string | number>;
@@ -19,6 +31,8 @@ export interface NormalizedComboConfig {
19
31
  strategy: OcxComboStrategy;
20
32
  stickyLimit: number;
21
33
  defaultEffort: OcxComboDefaultEffort | null;
34
+ /** Trimmed public alias, or null when the combo keeps the default `combo/<id>` slug. */
35
+ alias: string | null;
22
36
  targets: Array<Required<OcxComboTarget>>;
23
37
  }
24
38
 
@@ -37,11 +51,88 @@ export function comboModelId(id: string): string {
37
51
  return `${COMBO_NAMESPACE}/${id}`;
38
52
  }
39
53
 
54
+ /** Public model id clients request: the alias when set, else the default `combo/<id>`. */
55
+ export function comboPublicModelId(id: string, combo: { alias?: string | null }): string {
56
+ const alias = typeof combo.alias === "string" ? combo.alias.trim() : "";
57
+ return alias || comboModelId(id);
58
+ }
59
+
60
+ /**
61
+ * Resolve a client-requested model id to a combo config key. The canonical `combo/<id>`
62
+ * form wins first (back-compat); otherwise an exact alias match across configured combos.
63
+ */
64
+ export function resolveComboId(
65
+ config: { combos?: Record<string, OcxComboConfig> },
66
+ modelId: string,
67
+ ): string | null {
68
+ const direct = parseComboModelId(modelId);
69
+ if (direct) return direct;
70
+ const combos = config.combos;
71
+ if (!combos) return null;
72
+ for (const [id, raw] of Object.entries(combos)) {
73
+ if (!raw || typeof raw !== "object") continue;
74
+ const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
75
+ if (alias && alias === modelId) return id;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Cross-combo alias checks that need the full combos map (uniqueness). Kept separate
82
+ * from `comboConfigIssues` so config-file validation and the management API share it.
83
+ */
84
+ export function comboAliasIssues(
85
+ id: string,
86
+ alias: string,
87
+ combos: Record<string, OcxComboConfig> | undefined,
88
+ options: { excludeComboId?: string } = {},
89
+ ): ComboValidationIssue[] {
90
+ const issues: ComboValidationIssue[] = [];
91
+ if (!COMBO_ALIAS_PATTERN.test(alias)) {
92
+ issues.push({
93
+ path: ["alias"],
94
+ message: "alias must use letters, numbers, dot, underscore, or hyphen, with at most one \"/\" segment",
95
+ });
96
+ return issues;
97
+ }
98
+ if (alias === COMBO_NAMESPACE || alias.startsWith(`${COMBO_NAMESPACE}/`)) {
99
+ issues.push({
100
+ path: ["alias"],
101
+ message: `alias must not use the reserved "${COMBO_NAMESPACE}/" namespace`,
102
+ });
103
+ }
104
+ if (!alias.includes("/") && NATIVE_OPENAI_FAMILY_PATTERN.test(alias)) {
105
+ issues.push({
106
+ path: ["alias"],
107
+ message: "bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are not allowed",
108
+ });
109
+ }
110
+ for (const [otherId, other] of Object.entries(combos ?? {})) {
111
+ if (otherId === id || otherId === options.excludeComboId) continue;
112
+ const otherAlias = typeof other?.alias === "string" ? other.alias.trim() : "";
113
+ if (otherAlias && otherAlias === alias) {
114
+ issues.push({
115
+ path: ["alias"],
116
+ message: `alias "${alias}" is already used by combo "${otherId}"`,
117
+ });
118
+ }
119
+ }
120
+ return issues;
121
+ }
122
+
123
+ export interface ComboValidationOptions {
124
+ requireEnabledTarget?: boolean;
125
+ /** Full combos map for alias uniqueness checks; omitted during early config load. */
126
+ combos?: Record<string, OcxComboConfig>;
127
+ /** Combo being renamed — its stored alias is excluded from uniqueness checks. */
128
+ excludeComboId?: string;
129
+ }
130
+
40
131
  export function comboConfigIssues(
41
132
  id: string,
42
133
  raw: unknown,
43
134
  providers: Record<string, OcxProviderConfig>,
44
- options: { requireEnabledTarget?: boolean } = {},
135
+ options: ComboValidationOptions = {},
45
136
  ): ComboValidationIssue[] {
46
137
  const issues: ComboValidationIssue[] = [];
47
138
  if (!isValidComboId(id)) {
@@ -88,6 +179,15 @@ export function comboConfigIssues(
88
179
  });
89
180
  }
90
181
 
182
+ if (body.alias !== undefined) {
183
+ if (typeof body.alias !== "string") {
184
+ issues.push({ path: ["alias"], message: "alias must be a string" });
185
+ } else {
186
+ const alias = body.alias.trim();
187
+ if (alias) issues.push(...comboAliasIssues(id, alias, options.combos, options));
188
+ }
189
+ }
190
+
91
191
  if (!Array.isArray(body.targets) || body.targets.length === 0) {
92
192
  issues.push({ path: ["targets"], message: "targets must be a non-empty array" });
93
193
  return issues;
@@ -155,16 +255,18 @@ export function comboConfigError(
155
255
  id: string,
156
256
  raw: unknown,
157
257
  providers: Record<string, OcxProviderConfig>,
158
- options: { requireEnabledTarget?: boolean } = {},
258
+ options: ComboValidationOptions = {},
159
259
  ): string | null {
160
260
  return comboConfigIssues(id, raw, providers, options)[0]?.message ?? null;
161
261
  }
162
262
 
163
263
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
264
+ const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
164
265
  return {
165
266
  strategy: raw.strategy ?? "failover",
166
267
  stickyLimit: raw.stickyLimit ?? 1,
167
268
  defaultEffort: raw.defaultEffort ?? null,
269
+ alias: alias || null,
168
270
  targets: raw.targets.map(target => ({
169
271
  provider: target.provider.trim(),
170
272
  model: target.model.trim(),
package/src/config.ts CHANGED
@@ -329,8 +329,14 @@ const warnedConfigFallbacks = new Set<string>();
329
329
  const providerConfigSchema = z.object({
330
330
  adapter: z.string().min(1),
331
331
  baseUrl: z.string().min(1),
332
+ responsesPath: z.string().min(1).optional(),
332
333
  allowPrivateNetwork: z.boolean().optional(),
333
334
  codexAccountMode: z.enum(["pool", "direct"]).optional(),
335
+ responsesItemIdRepair: z.object({
336
+ message: z.array(z.string().min(1)).optional(),
337
+ reasoning: z.array(z.string().min(1)).optional(),
338
+ repairMissingTerminalIds: z.boolean().optional(),
339
+ }).strict().optional(),
334
340
  }).passthrough();
335
341
 
336
342
  const RESERVED_PROVIDER_NAMES = new Set(["__proto__", "prototype", "constructor"]);
@@ -369,6 +375,18 @@ export function providerBaseUrlConfigError(baseUrl: string): string | null {
369
375
  return null;
370
376
  }
371
377
 
378
+ function providerResponsesPathConfigError(responsesPath: string | undefined): string | null {
379
+ if (responsesPath === undefined) return null;
380
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(responsesPath) || responsesPath.includes("://")) {
381
+ return "responsesPath must be a relative path without a URL scheme";
382
+ }
383
+ if (!responsesPath.startsWith("/")) return "responsesPath must start with /";
384
+ if (responsesPath.includes("?") || responsesPath.includes("#")) {
385
+ return "responsesPath must not include query strings or fragments";
386
+ }
387
+ return null;
388
+ }
389
+
372
390
  export function providerHeadersConfigError(headers: unknown): string | null {
373
391
  if (headers === undefined) return null;
374
392
  if (!headers || typeof headers !== "object" || Array.isArray(headers)) return "headers must be an object";
@@ -396,6 +414,14 @@ export function positiveIntegerRecordConfigError(value: unknown, field: string):
396
414
  return null;
397
415
  }
398
416
 
417
+ export function positiveIntegerConfigError(value: unknown, field: string): string | null {
418
+ if (value === undefined) return null;
419
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
420
+ return `${field} must be a positive finite integer`;
421
+ }
422
+ return null;
423
+ }
424
+
399
425
  const configSchema = z.object({
400
426
  port: z.number().int().min(0).max(65535).default(10100),
401
427
  providers: z.record(z.string(), providerConfigSchema),
@@ -403,6 +429,7 @@ const configSchema = z.object({
403
429
  openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
404
430
  providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
405
431
  contextCapValue: z.number().int().positive().optional(),
432
+ multiAgentGuidanceEnabled: z.boolean().optional(),
406
433
  }).passthrough().superRefine((config, ctx) => {
407
434
  for (const name of Object.keys(config.providers)) {
408
435
  if (!isValidProviderName(name)) {
@@ -451,6 +478,14 @@ const configSchema = z.object({
451
478
  });
452
479
  }
453
480
  }
481
+ const responsesPathError = providerResponsesPathConfigError(provider.responsesPath);
482
+ if (responsesPathError) {
483
+ ctx.addIssue({
484
+ code: "custom",
485
+ path: ["providers", name, "responsesPath"],
486
+ message: responsesPathError,
487
+ });
488
+ }
454
489
  const headersError = providerHeadersConfigError((provider as { headers?: unknown }).headers);
455
490
  if (headersError) {
456
491
  ctx.addIssue({
@@ -470,6 +505,28 @@ const configSchema = z.object({
470
505
  message: maxInputError,
471
506
  });
472
507
  }
508
+ const defaultMaxOutputError = positiveIntegerConfigError(
509
+ (provider as { defaultMaxOutputTokens?: unknown }).defaultMaxOutputTokens,
510
+ "defaultMaxOutputTokens",
511
+ );
512
+ if (defaultMaxOutputError) {
513
+ ctx.addIssue({
514
+ code: "custom",
515
+ path: ["providers", name, "defaultMaxOutputTokens"],
516
+ message: defaultMaxOutputError,
517
+ });
518
+ }
519
+ const maxOutputError = positiveIntegerRecordConfigError(
520
+ (provider as { modelMaxOutputTokens?: unknown }).modelMaxOutputTokens,
521
+ "modelMaxOutputTokens",
522
+ );
523
+ if (maxOutputError) {
524
+ ctx.addIssue({
525
+ code: "custom",
526
+ path: ["providers", name, "modelMaxOutputTokens"],
527
+ message: maxOutputError,
528
+ });
529
+ }
473
530
  if (Object.hasOwn(provider, "codexAccountMode") && provider.codexAccountMode !== undefined) {
474
531
  // Persisted account mode is valid ONLY on the canonical built-in `openai` forward provider.
475
532
  // Old openai-multi rows stay parseable (they never carry a mode) so startup can migrate them.
@@ -500,7 +557,12 @@ const configSchema = z.object({
500
557
  ctx.addIssue({ code: "custom", path: ["combos"], message: "combos must be an object" });
501
558
  } else {
502
559
  for (const [id, raw] of Object.entries(combos as Record<string, unknown>)) {
503
- for (const issue of comboConfigIssues(id, raw, config.providers)) {
560
+ // Pass the full map so cross-combo rules (alias uniqueness) apply at load time
561
+ // too, not just via the management API; each combo is excluded from its own check.
562
+ for (const issue of comboConfigIssues(id, raw, config.providers, {
563
+ combos: combos as Record<string, import("./types").OcxComboConfig>,
564
+ excludeComboId: id,
565
+ })) {
504
566
  ctx.addIssue({
505
567
  code: "custom",
506
568
  path: ["combos", id, ...issue.path],
@@ -688,6 +750,12 @@ export function codexAutoStartEnabled(config: Pick<OcxConfig, "codexAutoStart">)
688
750
  return config.codexAutoStart !== false;
689
751
  }
690
752
 
753
+ export function multiAgentGuidanceEnabled(
754
+ config: Pick<OcxConfig, "multiAgentGuidanceEnabled">,
755
+ ): boolean {
756
+ return config.multiAgentGuidanceEnabled !== false;
757
+ }
758
+
691
759
  export function getDefaultConfig(): OcxConfig {
692
760
  // Fresh-install default: works out of the box with Codex's ChatGPT OAuth (no API key).
693
761
  // gpt-* requests forward the caller's incoming OAuth headers to the ChatGPT backend.
@@ -708,6 +776,7 @@ export function getDefaultConfig(): OcxConfig {
708
776
  },
709
777
  defaultProvider: "openai",
710
778
  subagentModels: [...DEFAULT_SUBAGENT_MODELS],
779
+ multiAgentGuidanceEnabled: true,
711
780
  websockets: false,
712
781
  codexAutoStart: true,
713
782
  };
@@ -24,6 +24,7 @@ const MESSAGE_CRC_LEN = 4;
24
24
  const HEADER_BLOCK_OFFSET = PRELUDE_LEN + PRELUDE_CRC_LEN;
25
25
  const MIN_MESSAGE_LEN = HEADER_BLOCK_OFFSET + MESSAGE_CRC_LEN;
26
26
  const MAX_MESSAGE_LEN = 16 * 1024 * 1024;
27
+ const MAX_HEADERS_LEN = 128 * 1024;
27
28
 
28
29
  export interface EventStreamMessage {
29
30
  /** Header casing is preserved verbatim (e.g. `:event-type`, `:message-type`). */
@@ -62,6 +63,7 @@ export function decodeMessage(frame: Uint8Array): EventStreamMessage {
62
63
  const preludeCrc = view.getUint32(8, false);
63
64
  const computedPreludeCrc = crc32(frame.subarray(0, PRELUDE_LEN));
64
65
  if (computedPreludeCrc !== preludeCrc) throw new Error("eventstream: prelude CRC mismatch");
66
+ if (headersLen > MAX_HEADERS_LEN) throw new Error(`eventstream: headers length ${headersLen} exceeds maximum`);
65
67
  if (headersLen > total - MIN_MESSAGE_LEN) throw new Error("eventstream: headers length exceeds frame payload");
66
68
  const msgCrc = view.getUint32(total - MESSAGE_CRC_LEN, false);
67
69
  const computedMsgCrc = crc32(frame.subarray(0, total - MESSAGE_CRC_LEN));
@@ -185,6 +187,13 @@ export async function* decodeEventStream(source: ReadableStream<Uint8Array>): As
185
187
  const total = dv.getUint32(0, false);
186
188
  if (total < MIN_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} below minimum`);
187
189
  if (total > MAX_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} exceeds maximum`);
190
+ if (buf.length - offset >= HEADER_BLOCK_OFFSET) {
191
+ const headersLen = dv.getUint32(4, false);
192
+ const preludeCrc = dv.getUint32(8, false);
193
+ const computedPreludeCrc = crc32(buf.subarray(offset, offset + PRELUDE_LEN));
194
+ if (computedPreludeCrc !== preludeCrc) throw new Error("eventstream: prelude CRC mismatch");
195
+ if (headersLen > MAX_HEADERS_LEN) throw new Error(`eventstream: headers length ${headersLen} exceeds maximum`);
196
+ }
188
197
  if (buf.length - offset < total) break;
189
198
  const frame = buf.subarray(offset, offset + total);
190
199
  yield decodeMessage(frame);
@@ -219,7 +219,7 @@ export async function getValidAccessTokenSnapshot(provider: string): Promise<OAu
219
219
  }
220
220
 
221
221
  /** Providers whose upstream-401 replay path may force a snapshot refresh. */
222
- const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot"]);
222
+ const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro"]);
223
223
 
224
224
  export async function forceRefreshOAuthAccessSnapshot(
225
225
  rejected: OAuthAccessSnapshot,
@@ -478,6 +478,8 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
478
478
  "models",
479
479
  "contextWindow",
480
480
  "modelContextWindows",
481
+ "defaultMaxOutputTokens",
482
+ "modelMaxOutputTokens",
481
483
  "modelInputModalities",
482
484
  "noReasoningModels",
483
485
  "noVisionModels",
@@ -6,8 +6,17 @@ import { Database } from "bun:sqlite";
6
6
  const DEFAULT_EXPIRES_MS = 3600_000;
7
7
  const KIRO_REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d$/;
8
8
  const CLIENT_ID_HASH_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
9
- const TOKEN_KEYS = ["kirocli:social:token", "kirocli:odic:token", "codewhisperer:odic:token"];
10
- const REGISTRATION_KEYS = ["kirocli:odic:device-registration", "codewhisperer:odic:device-registration"];
9
+ const TOKEN_KEYS = [
10
+ "kirocli:odic:token",
11
+ "kirocli:oidc:token",
12
+ "kirocli:social:token",
13
+ "codewhisperer:odic:token",
14
+ ];
15
+ const REGISTRATION_KEYS = [
16
+ "kirocli:odic:device-registration",
17
+ "kirocli:oidc:device-registration",
18
+ "codewhisperer:odic:device-registration",
19
+ ];
11
20
 
12
21
  export type KiroAuthType = "kiro_desktop" | "aws_sso_oidc";
13
22
  export type KiroCredentialSource = "json" | "sqlite";
@@ -17,6 +26,8 @@ export type KiroDiagnosticStatus =
17
26
  | "schema_mismatch"
18
27
  | "invalid_json"
19
28
  | "token_missing"
29
+ | "token_ambiguous"
30
+ | "token_key_missing"
20
31
  | "token_found"
21
32
  | "registration_found";
22
33
 
@@ -99,15 +110,27 @@ function jsonCredentialPaths(): string[] {
99
110
 
100
111
  function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {
101
112
  const home = userHome();
102
- const entries: Array<{ location: KiroImportDiagnostic["location"]; path: string }> = [];
103
- if (process.env.KIRO_CLI_DB_FILE) entries.push({ location: "kiro-cli-db-env", path: expandPath(process.env.KIRO_CLI_DB_FILE) });
104
- entries.push(
113
+ const configured = process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim();
114
+ if (configured) return [{ location: "kiro-cli-db-env", path: expandPath(configured) }];
115
+ return [
105
116
  { location: "kiro-cli-data", path: join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") },
106
117
  { location: "kiro-cli-linux-data", path: join(home, ".local", "share", "kiro-cli", "data.sqlite3") },
107
118
  { location: "amazon-q-data", path: join(home, ".local", "share", "amazon-q", "data.sqlite3") },
108
119
  { location: "kiro-sso-cache", path: join(home, ".kiro", "sso", "cache.db") },
109
- );
110
- return entries;
120
+ ];
121
+ }
122
+
123
+ function selectTokenRow(db: Database): { value: string } | null | "ambiguous" | "selected_missing" {
124
+ const rows = db.query("SELECT key, value FROM auth_kv WHERE key LIKE ? ORDER BY key ASC").all("%:token") as Array<{ key: string; value: string }>;
125
+ const selectedKey = process.env.KIROCLI_TOKEN_KEY?.trim();
126
+ if (selectedKey) return rows.find(row => row.key === selectedKey) ?? "selected_missing";
127
+ for (const preferred of TOKEN_KEYS) {
128
+ const row = rows.find(candidate => candidate.key === preferred);
129
+ if (row) return row;
130
+ }
131
+ if (rows.length === 0) return null;
132
+ if (rows.length === 1) return rows[0];
133
+ return "ambiguous";
111
134
  }
112
135
 
113
136
  function credentialFromJson(data: JsonObject, source: KiroCredentialSource): ImportedKiroCredential | undefined {
@@ -202,22 +225,26 @@ function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKir
202
225
  continue;
203
226
  }
204
227
  try {
205
- let tokenData: JsonObject | undefined;
206
- for (const key of TOKEN_KEYS) {
207
- const row = db.query("SELECT value FROM auth_kv WHERE key = ?").get(key) as { value: string } | null;
208
- if (!row) continue;
209
- try {
210
- tokenData = JSON.parse(row.value) as JsonObject;
211
- } catch {
212
- diagnostics.push({ location, status: "invalid_json" });
213
- continue;
214
- }
215
- if (stringField(tokenData, "access_token", "accessToken")) break;
228
+ const row = selectTokenRow(db);
229
+ if (row === "ambiguous") {
230
+ diagnostics.push({ location, status: "token_ambiguous" });
231
+ throw new Error("Kiro CLI credential database contains multiple tokens; set KIROCLI_TOKEN_KEY to select one");
232
+ }
233
+ if (row === "selected_missing") {
234
+ diagnostics.push({ location, status: "token_key_missing" });
235
+ throw new Error("The KIROCLI_TOKEN_KEY selection was not found in the Kiro CLI credential database");
216
236
  }
217
- if (!tokenData) {
237
+ if (!row) {
218
238
  diagnostics.push({ location, status: "token_missing" });
219
239
  continue;
220
240
  }
241
+ let tokenData: JsonObject;
242
+ try {
243
+ tokenData = JSON.parse(row.value) as JsonObject;
244
+ } catch {
245
+ diagnostics.push({ location, status: "invalid_json" });
246
+ continue;
247
+ }
221
248
  let registrationData: JsonObject = {};
222
249
  for (const key of REGISTRATION_KEYS) {
223
250
  const row = db.query("SELECT value FROM auth_kv WHERE key = ?").get(key) as { value: string } | null;
@@ -235,7 +262,8 @@ function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKir
235
262
  const credential = credentialFromJson(merged, "sqlite");
236
263
  diagnostics.push({ location, status: credential ? "token_found" : "token_missing" });
237
264
  if (credential) return credential;
238
- } catch {
265
+ } catch (error) {
266
+ if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) throw error;
239
267
  diagnostics.push({ location, status: "schema_mismatch" });
240
268
  } finally {
241
269
  db.close();
@@ -72,6 +72,8 @@ export function providerConfigFromKeyLoginProvider(def: KeyLoginProvider, key: s
72
72
  ...(def.contextWindow !== undefined ? { contextWindow: def.contextWindow } : {}),
73
73
  ...(def.modelContextWindows ? { modelContextWindows: { ...def.modelContextWindows } } : {}),
74
74
  ...(def.modelMaxInputTokens ? { modelMaxInputTokens: { ...def.modelMaxInputTokens } } : {}),
75
+ ...(def.defaultMaxOutputTokens !== undefined ? { defaultMaxOutputTokens: def.defaultMaxOutputTokens } : {}),
76
+ ...(def.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...def.modelMaxOutputTokens } } : {}),
75
77
  ...(def.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(def.modelInputModalities) } : {}),
76
78
  ...(def.reasoningEfforts ? { reasoningEfforts: [...def.reasoningEfforts] } : {}),
77
79
  ...(def.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(def.modelReasoningEfforts) } : {}),
@@ -13,6 +13,8 @@ export interface DerivedKeyLoginProvider {
13
13
  modelContextWindows?: Record<string, number>;
14
14
  modelInputModalities?: Record<string, string[]>;
15
15
  modelMaxInputTokens?: Record<string, number>;
16
+ defaultMaxOutputTokens?: number;
17
+ modelMaxOutputTokens?: Record<string, number>;
16
18
  reasoningEfforts?: string[];
17
19
  modelReasoningEfforts?: Record<string, string[]>;
18
20
  modelDefaultReasoningEfforts?: Record<string, string>;
@@ -96,6 +98,8 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
96
98
  ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
97
99
  ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
98
100
  ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: { ...entry.modelMaxInputTokens } } : {}),
101
+ ...(entry.defaultMaxOutputTokens !== undefined ? { defaultMaxOutputTokens: entry.defaultMaxOutputTokens } : {}),
102
+ ...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...entry.modelMaxOutputTokens } } : {}),
99
103
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
100
104
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
101
105
  ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
@@ -135,6 +139,8 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
135
139
  ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
136
140
  ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
137
141
  ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: { ...entry.modelMaxInputTokens } } : {}),
142
+ ...(entry.defaultMaxOutputTokens !== undefined ? { defaultMaxOutputTokens: entry.defaultMaxOutputTokens } : {}),
143
+ ...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...entry.modelMaxOutputTokens } } : {}),
138
144
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
139
145
  ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
140
146
  ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
@@ -203,6 +209,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
203
209
  if (prov.contextWindow === undefined && seed.contextWindow !== undefined) prov.contextWindow = seed.contextWindow;
204
210
  if (!prov.modelContextWindows && seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows };
205
211
  if (!prov.modelInputModalities && seed.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(seed.modelInputModalities);
212
+ if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
213
+ if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
206
214
  if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
207
215
  if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.modelReasoningEfforts);
208
216
  if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
@@ -45,8 +45,8 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
45
45
 
46
46
  const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
47
47
 
48
- // Kiro has no upstream reasoning_effort enum; these labels map to fake-thinking budgets in
49
- // src/adapters/kiro.ts.
48
+ // gpt-5.6-sol sends these values through Kiro's verified native reasoning field. Other models map
49
+ // them to bounded thinking instructions until their native effort support is verified.
50
50
  export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
51
51
  KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
52
52
  );
@@ -7,6 +7,7 @@ import {
7
7
  type CodexAuthContext,
8
8
  } from "../codex/auth-context";
9
9
  import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
10
+ import { extractAccountId } from "../oauth/chatgpt";
10
11
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../server/auth-cors";
11
12
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
12
13
  import {
@@ -47,6 +48,23 @@ export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiFor
47
48
  }];
48
49
  }
49
50
 
51
+ function directSidecarHeaders(
52
+ incomingHeaders: Headers,
53
+ ): Headers | undefined {
54
+ const bearer = incomingHeaders.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
55
+ if (!bearer) return undefined;
56
+ const derivedAccountId = extractAccountId(undefined, bearer);
57
+ if (!derivedAccountId) return undefined;
58
+ const requestedAccountId = incomingHeaders.get("chatgpt-account-id")?.trim();
59
+ // JWT payloads are decoded locally but not signature-verified. Requiring the caller's
60
+ // explicit account header, and checking it against the token claim, makes forwarding an
61
+ // intentional ChatGPT-auth operation instead of silently reclassifying any JWT-shaped
62
+ // provider credential as a Codex bearer.
63
+ if (!requestedAccountId || requestedAccountId !== derivedAccountId) return undefined;
64
+ const selected = headersForCodexAuthContext(incomingHeaders, { kind: "main", accountId: null });
65
+ return selected;
66
+ }
67
+
50
68
  export async function resolveFirstUsableOpenAiSidecar(
51
69
  candidates: readonly OpenAiForwardSidecarCandidate[],
52
70
  incomingHeaders: Headers,
@@ -60,7 +78,16 @@ export async function resolveFirstUsableOpenAiSidecar(
60
78
  callerBearerMayBeForwarded = false;
61
79
  }
62
80
  for (const candidate of candidates) {
63
- if (candidate.accountMode === "direct" && (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders))) continue;
81
+ if (candidate.accountMode === "direct") {
82
+ if (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders)) continue;
83
+ const headers = directSidecarHeaders(incomingHeaders);
84
+ if (!headers) continue;
85
+ return {
86
+ ...candidate,
87
+ authContext: { kind: "main", accountId: null },
88
+ headers,
89
+ };
90
+ }
64
91
  const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode);
65
92
  if (!isCodexAuthContextUsable(authContext, config)) continue;
66
93
  return {