@adaptic/lumic-utils 1.0.28 → 1.0.30

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 (33) hide show
  1. package/dist/{apollo-client.client-CzTlfziA.js → apollo-client.client-CNrz3Xod.js} +3 -3
  2. package/dist/{apollo-client.client-CzTlfziA.js.map → apollo-client.client-CNrz3Xod.js.map} +1 -1
  3. package/dist/{apollo-client.client-KzZx5slz.js → apollo-client.client-DsFSBlst.js} +4 -4
  4. package/dist/{apollo-client.client-KzZx5slz.js.map → apollo-client.client-DsFSBlst.js.map} +1 -1
  5. package/dist/{apollo-client.server-DQPc4Iam.js → apollo-client.server-DMz8cEcx.js} +3 -3
  6. package/dist/{apollo-client.server-DQPc4Iam.js.map → apollo-client.server-DMz8cEcx.js.map} +1 -1
  7. package/dist/{apollo-client.server-Bv2rQsCw.js → apollo-client.server-YkSyk6OJ.js} +3 -3
  8. package/dist/{apollo-client.server-Bv2rQsCw.js.map → apollo-client.server-YkSyk6OJ.js.map} +1 -1
  9. package/dist/{index-I-0uzUiF.js → index-BZ9x9UfI.js} +734 -55
  10. package/dist/{index-FqzV71J4.js.map → index-BZ9x9UfI.js.map} +1 -1
  11. package/dist/{index-Cjy8RH6E.js → index-CGlW9JSL.js} +2 -2
  12. package/dist/{index-Cjy8RH6E.js.map → index-CGlW9JSL.js.map} +1 -1
  13. package/dist/{index-FqzV71J4.js → index-DlAW691N.js} +735 -54
  14. package/dist/{index-I-0uzUiF.js.map → index-DlAW691N.js.map} +1 -1
  15. package/dist/{index-QMP2OpYn.js → index-VsQMFi2M.js} +2 -2
  16. package/dist/{index-QMP2OpYn.js.map → index-VsQMFi2M.js.map} +1 -1
  17. package/dist/index.cjs +3 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.mjs +1 -1
  20. package/dist/test.cjs +1 -1
  21. package/dist/test.mjs +1 -1
  22. package/dist/types/config/secrets.d.ts +10 -0
  23. package/dist/types/functions/__tests__/gpt56-model-release-layer.test.d.ts +1 -0
  24. package/dist/types/functions/__tests__/llm-openai-responses-routing.test.d.ts +1 -0
  25. package/dist/types/functions/llm-config.d.ts +8 -6
  26. package/dist/types/functions/llm-openai.d.ts +125 -2
  27. package/dist/types/index.d.ts +1 -1
  28. package/dist/types/schemas/openai-schemas.d.ts +30 -30
  29. package/dist/types/schemas/perplexity-schemas.d.ts +40 -40
  30. package/dist/types/types/openai-types.d.ts +44 -5
  31. package/dist/types/utils/config.d.ts +12 -1
  32. package/dist/types/utils/llm-cost-tracker.d.ts +2 -2
  33. package/package.json +2 -2
@@ -100,9 +100,14 @@ const OPENAI_COMPATIBLE_PROVIDERS = {
100
100
  */
101
101
  const SUPPORTED_MODELS = {
102
102
  // ── OpenAI GPT-5.6 series (current flagship) ─────────────────────────
103
- // sol / terra / luna are the 2026 flagship trio (developers.openai.com/
104
- // api/docs/models/all). Same GPT-5.x API surface + the temperature
105
- // constraint documented below.
103
+ // sol / terra / luna are the 2026 flagship reasoning trio
104
+ // (developers.openai.com/api/docs/models/all). They are REASONING models
105
+ // (isReasoningModel: true) and — like all reasoning models — do not accept a
106
+ // custom temperature. Critically, OpenAI rejects function tools for them on
107
+ // Chat Completions ("Function tools with reasoning_effort are not supported
108
+ // for gpt-5.6-sol in /v1/chat/completions"), so `requiresResponsesApiForTools`
109
+ // routes tool-bearing calls to the Responses API. Both flags are corrected
110
+ // here from their initial (registration-time) `false` values.
106
111
  'gpt-5.6-sol': {
107
112
  provider: 'openai',
108
113
  supportsTools: true,
@@ -110,7 +115,8 @@ const SUPPORTED_MODELS = {
110
115
  supportsStructuredOutput: true,
111
116
  supportsDeveloperPrompt: true,
112
117
  supportsTemperature: false,
113
- isReasoningModel: false,
118
+ isReasoningModel: true,
119
+ requiresResponsesApiForTools: true,
114
120
  },
115
121
  'gpt-5.6-terra': {
116
122
  provider: 'openai',
@@ -119,7 +125,8 @@ const SUPPORTED_MODELS = {
119
125
  supportsStructuredOutput: true,
120
126
  supportsDeveloperPrompt: true,
121
127
  supportsTemperature: false,
122
- isReasoningModel: false,
128
+ isReasoningModel: true,
129
+ requiresResponsesApiForTools: true,
123
130
  },
124
131
  'gpt-5.6-luna': {
125
132
  provider: 'openai',
@@ -128,7 +135,8 @@ const SUPPORTED_MODELS = {
128
135
  supportsStructuredOutput: true,
129
136
  supportsDeveloperPrompt: true,
130
137
  supportsTemperature: false,
131
- isReasoningModel: false,
138
+ isReasoningModel: true,
139
+ requiresResponsesApiForTools: true,
132
140
  },
133
141
  // ── OpenAI GPT-5.5 series ────────────────────────────────────────────
134
142
  // GPT-5.x series: OpenAI's 2026 API constraint — only the default
@@ -611,24 +619,33 @@ function getModelProvider(model) {
611
619
  return SUPPORTED_MODELS[model].provider;
612
620
  }
613
621
  /**
614
- * Default model tiers per provider for use with LLM_MODEL_MINI/NORMAL/ADVANCED env vars
622
+ * Default model tiers per provider for use with the
623
+ * LLM_MODEL_MINI/NORMAL/ADVANCED/CRITICAL env vars.
624
+ *
625
+ * Four tiers, increasing in capability/cost: `mini` (high-volume, cheap) →
626
+ * `normal` (default) → `advanced` → `critical` (most-capable). The tier a caller
627
+ * lands on is resolved in `../utils/config` (see `LLM_MODEL_*`).
615
628
  */
616
629
  const PROVIDER_DEFAULT_MODELS = {
617
- // advanced pins to gpt-5.5, NOT the newer gpt-5.6-sol, deliberately: gpt-5.6-sol is a
618
- // reasoning model and OpenAI rejects function tools on /v1/chat/completions for it
619
- // ("Function tools with reasoning_effort are not supported for gpt-5.6-sol in
620
- // /v1/chat/completions") it requires the Responses API. Consumers that call
621
- // makeOpenAIChatCompletionCall WITH tools (e.g. the engine's decision path) 400 on
622
- // every call. gpt-5.6-sol/terra/luna remain REGISTERED in SUPPORTED_MODELS for when a
623
- // Responses-API routing path lands; until then the advanced (tool-calling) tier pins
624
- // to gpt-5.5 — the newest model compatible with chat-completions + function tools.
625
- openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.4-mini', advanced: 'gpt-5.5' },
626
- anthropic: { mini: 'claude-haiku-4-5', normal: 'claude-sonnet-4-6', advanced: 'claude-opus-4-7' },
627
- deepseek: { mini: 'deepseek-v4-flash', normal: 'deepseek-v4-flash', advanced: 'deepseek-v4-pro' },
628
- kimi: { mini: 'kimi-k2-0905-preview', normal: 'kimi-k2.5', advanced: 'kimi-k2.6' },
629
- qwen: { mini: 'qwen3.5-flash', normal: 'qwen3.5-plus', advanced: 'qwen3-max' },
630
- xai: { mini: 'grok-4.1-fast', normal: 'grok-4-fast-non-reasoning', advanced: 'grok-4.3' },
631
- gemini: { mini: 'gemini-3.1-flash-lite-preview', normal: 'gemini-3-flash-preview', advanced: 'gemini-3.1-pro-preview' },
630
+ // OpenAI now leads with the GPT-5.6 reasoning trio. The historical blocker
631
+ // (gpt-5.6-* returning 400 on Chat-Completions + tools) is resolved by the
632
+ // Responses-API routing (`requiresResponsesApiForTools` +
633
+ // `makeOpenAIChatCompletionCall`), so the intended canary is live:
634
+ // normal → gpt-5.6-luna (cheapest of the trio)
635
+ // advanced gpt-5.6-terra (flips off the temporary gpt-5.5 pin)
636
+ // critical → gpt-5.6-sol (most capable; in/out priced like gpt-5.5)
637
+ // `mini` stays on gpt-5.4-nano — the high-volume summarizer tier where the
638
+ // trio's reasoning cost is not warranted.
639
+ openai: { mini: 'gpt-5.4-nano', normal: 'gpt-5.6-luna', advanced: 'gpt-5.6-terra', critical: 'gpt-5.6-sol' },
640
+ // Non-OpenAI providers: `critical` mirrors `advanced` (each provider's
641
+ // most-capable model), so the new tier is a no-op for them until a distinct
642
+ // critical-tier model is chosen.
643
+ anthropic: { mini: 'claude-haiku-4-5', normal: 'claude-sonnet-4-6', advanced: 'claude-opus-4-7', critical: 'claude-opus-4-7' },
644
+ deepseek: { mini: 'deepseek-v4-flash', normal: 'deepseek-v4-flash', advanced: 'deepseek-v4-pro', critical: 'deepseek-v4-pro' },
645
+ kimi: { mini: 'kimi-k2-0905-preview', normal: 'kimi-k2.5', advanced: 'kimi-k2.6', critical: 'kimi-k2.6' },
646
+ qwen: { mini: 'qwen3.5-flash', normal: 'qwen3.5-plus', advanced: 'qwen3-max', critical: 'qwen3-max' },
647
+ xai: { mini: 'grok-4.1-fast', normal: 'grok-4-fast-non-reasoning', advanced: 'grok-4.3', critical: 'grok-4.3' },
648
+ gemini: { mini: 'gemini-3.1-flash-lite-preview', normal: 'gemini-3-flash-preview', advanced: 'gemini-3.1-pro-preview', critical: 'gemini-3.1-pro-preview' },
632
649
  };
633
650
 
634
651
  // Default implementation using console (backward compatible)
@@ -703,9 +720,11 @@ const lumicSecretsSchema = zod.z.object({
703
720
  llmMiniProvider: zod.z.string().optional(),
704
721
  llmNormalProvider: zod.z.string().optional(),
705
722
  llmAdvancedProvider: zod.z.string().optional(),
723
+ llmCriticalProvider: zod.z.string().optional(),
706
724
  llmModelMini: zod.z.string().optional(),
707
725
  llmModelNormal: zod.z.string().optional(),
708
726
  llmModelAdvanced: zod.z.string().optional(),
727
+ llmModelCritical: zod.z.string().optional(),
709
728
  lumicDebug: zod.z.boolean().default(false),
710
729
  allowedUploadPaths: zod.z.string().default('/tmp'),
711
730
  }),
@@ -768,9 +787,11 @@ function loadSecrets() {
768
787
  llmMiniProvider: process.env.LLM_MINI_PROVIDER,
769
788
  llmNormalProvider: process.env.LLM_NORMAL_PROVIDER,
770
789
  llmAdvancedProvider: process.env.LLM_ADVANCED_PROVIDER,
790
+ llmCriticalProvider: process.env.LLM_CRITICAL_PROVIDER,
771
791
  llmModelMini: process.env.LLM_MODEL_MINI,
772
792
  llmModelNormal: process.env.LLM_MODEL_NORMAL,
773
793
  llmModelAdvanced: process.env.LLM_MODEL_ADVANCED,
794
+ llmModelCritical: process.env.LLM_MODEL_CRITICAL,
774
795
  lumicDebug: process.env.LUMIC_DEBUG === 'true' ||
775
796
  process.env.lumic_debug === 'true',
776
797
  allowedUploadPaths: process.env.LUMIC_ALLOWED_UPLOAD_PATHS || '/tmp',
@@ -870,6 +891,7 @@ const LLM_DEFAULT_PROVIDER = parseProvider(getSecrets().config.llmDefaultProvide
870
891
  const LLM_MINI_PROVIDER = parseProvider(getSecrets().config.llmMiniProvider) || LLM_DEFAULT_PROVIDER;
871
892
  const LLM_NORMAL_PROVIDER = parseProvider(getSecrets().config.llmNormalProvider) || LLM_DEFAULT_PROVIDER;
872
893
  const LLM_ADVANCED_PROVIDER = parseProvider(getSecrets().config.llmAdvancedProvider) || LLM_DEFAULT_PROVIDER;
894
+ const LLM_CRITICAL_PROVIDER = parseProvider(getSecrets().config.llmCriticalProvider) || LLM_DEFAULT_PROVIDER;
873
895
  // Keep backward compat — LLM_PROVIDER is the default provider
874
896
  const LLM_PROVIDER = LLM_DEFAULT_PROVIDER;
875
897
  /**
@@ -889,13 +911,14 @@ const DEFAULT_MODEL = (() => {
889
911
  * Model tier values resolved from env vars.
890
912
  *
891
913
  * Resolution order per tier:
892
- * 1. LLM_MODEL_MINI / LLM_MODEL_NORMAL / LLM_MODEL_ADVANCED (explicit model override)
914
+ * 1. LLM_MODEL_MINI / LLM_MODEL_NORMAL / LLM_MODEL_ADVANCED / LLM_MODEL_CRITICAL (explicit model override)
893
915
  * 2. PROVIDER_DEFAULT_MODELS[tier-specific provider][tier] (provider default for tier)
894
916
  *
895
917
  * The tier-specific provider is:
896
918
  * LLM_MINI_PROVIDER → falls back to LLM_DEFAULT_PROVIDER
897
919
  * LLM_NORMAL_PROVIDER → falls back to LLM_DEFAULT_PROVIDER
898
920
  * LLM_ADVANCED_PROVIDER → falls back to LLM_DEFAULT_PROVIDER
921
+ * LLM_CRITICAL_PROVIDER → falls back to LLM_DEFAULT_PROVIDER
899
922
  */
900
923
  const LLM_MODEL_MINI = (() => {
901
924
  const env = getSecrets().config.llmModelMini;
@@ -924,6 +947,21 @@ const LLM_MODEL_ADVANCED = (() => {
924
947
  }
925
948
  return PROVIDER_DEFAULT_MODELS[LLM_ADVANCED_PROVIDER].advanced;
926
949
  })();
950
+ /**
951
+ * The most-capable ("critical") model tier — reserved for the highest-stakes
952
+ * calls. Resolves from LLM_MODEL_CRITICAL, else the critical-tier default of the
953
+ * resolved LLM_CRITICAL_PROVIDER (see PROVIDER_DEFAULT_MODELS). For OpenAI this
954
+ * is gpt-5.6-sol; for other providers it mirrors their advanced tier.
955
+ */
956
+ const LLM_MODEL_CRITICAL = (() => {
957
+ const env = getSecrets().config.llmModelCritical;
958
+ if (env) {
959
+ const resolved = resolveModelName(env);
960
+ if (resolved)
961
+ return resolved;
962
+ }
963
+ return PROVIDER_DEFAULT_MODELS[LLM_CRITICAL_PROVIDER].critical;
964
+ })();
927
965
  const PERPLEXITY_MODEL = getSecrets().config.perplexityModel || DEFAULT_PERPLEXITY_MODEL;
928
966
  const PERPLEXITY_API_URL = 'https://api.perplexity.ai/chat/completions';
929
967
 
@@ -936,15 +974,40 @@ const DEFAULT_DEVELOPER_PROMPT = `
936
974
  Respond only with final content (e.g. code, a json or yaml object, a formatted string, or a markdown document) and nothing else. Do not reply with a preamble, introduction, or conclusion.
937
975
  `;
938
976
  /**
939
- * Token costs in USD per token. Last updated May 2026.
977
+ * Token costs in USD per token. Last updated July 2026.
940
978
  *
941
- * `cacheHitCost` reflects OpenAI's cached-input billing rate (~50% of the
942
- * standard input rate per OpenAI's prompt caching documentation). When set,
943
- * `calculateCost` splits prompt tokens into cached vs non-cached buckets and
944
- * applies the discount; when omitted, cached tokens are billed at full input
945
- * rate (a silent ~50% cost overstatement for cache-friendly workloads).
979
+ * `cacheHitCost` reflects each model's cached-input billing rate. For most
980
+ * models this is ~50% of the standard input rate (per OpenAI's prompt-caching
981
+ * documentation); the GPT-5.6 reasoning trio discounts cached input more
982
+ * aggressively, to 10% of the input rate. When set, `calculateCost` splits
983
+ * prompt tokens into cached vs non-cached buckets and applies the discount; when
984
+ * omitted, cached tokens are billed at the full input rate (a silent cost
985
+ * overstatement for cache-friendly workloads).
946
986
  */
947
987
  const openAiModelCosts = {
988
+ // GPT-5.6 reasoning trio (sol/terra/luna). Tiered per-1M-token pricing,
989
+ // verified against OpenAI's published price sheet on 2026-07-13:
990
+ // sol — $5.00 in / $0.50 cached / $30.00 out (in/out identical to gpt-5.5)
991
+ // terra — $2.50 in / $0.25 cached / $15.00 out
992
+ // luna — $1.00 in / $0.10 cached / $6.00 out
993
+ // Cached input is 10% of the input rate across the trio. Reasoning tokens are
994
+ // a SUBSET of output tokens and are billed at the output rate inside
995
+ // `calculateCost`, so callers must not add them on top of the output count.
996
+ 'gpt-5.6-sol': {
997
+ inputCost: 5 / 1_000_000,
998
+ cacheHitCost: 0.5 / 1_000_000,
999
+ outputCost: 30 / 1_000_000,
1000
+ },
1001
+ 'gpt-5.6-terra': {
1002
+ inputCost: 2.5 / 1_000_000,
1003
+ cacheHitCost: 0.25 / 1_000_000,
1004
+ outputCost: 15 / 1_000_000,
1005
+ },
1006
+ 'gpt-5.6-luna': {
1007
+ inputCost: 1 / 1_000_000,
1008
+ cacheHitCost: 0.1 / 1_000_000,
1009
+ outputCost: 6 / 1_000_000,
1010
+ },
948
1011
  'gpt-5.5': {
949
1012
  inputCost: 5 / 1_000_000,
950
1013
  cacheHitCost: 2.5 / 1_000_000,
@@ -2276,12 +2339,18 @@ class LLMCostTracker {
2276
2339
  * @param provider - The LLM provider ('openai' or 'deepseek')
2277
2340
  * @param model - The model name (e.g., 'gpt-5', 'deepseek-chat')
2278
2341
  * @param inputTokens - Number of input/prompt tokens
2279
- * @param outputTokens - Number of output/completion tokens
2280
- * @param reasoningTokens - Number of reasoning tokens (default: 0)
2342
+ * @param outputTokens - Total output/completion tokens, INCLUDING reasoning tokens
2343
+ * @param reasoningTokens - Reasoning tokens, a subset of outputTokens (default: 0). Billed at the output rate, not on top of it.
2281
2344
  * @param cacheHitTokens - Number of cache hit tokens (default: 0)
2282
2345
  */
2283
2346
  trackUsage(provider, model, inputTokens, outputTokens, reasoningTokens = 0, cacheHitTokens = 0) {
2284
- const cost = calculateCost(provider, model, inputTokens, outputTokens, reasoningTokens, cacheHitTokens);
2347
+ // Reasoning tokens are a SUBSET of output tokens for every provider tracked
2348
+ // here (they are counted within completion/output tokens and billed at the
2349
+ // output rate). calculateCost bills output AND reasoning separately, so cost
2350
+ // the non-reasoning remainder to avoid billing reasoning twice; the stored
2351
+ // record keeps the full outputTokens/reasoningTokens for reporting. For
2352
+ // non-reasoning calls reasoningTokens is 0, so this is a no-op.
2353
+ const cost = calculateCost(provider, model, inputTokens, outputTokens - reasoningTokens, reasoningTokens, cacheHitTokens);
2285
2354
  const record = {
2286
2355
  provider,
2287
2356
  model,
@@ -2598,6 +2667,105 @@ function isReasoningModel(model) {
2598
2667
  }
2599
2668
  return getModelCapabilities(model).isReasoningModel;
2600
2669
  }
2670
+ /**
2671
+ * Checks whether a model must use the OpenAI Responses API (`/v1/responses`)
2672
+ * for function-tool calls rather than Chat Completions (`/v1/chat/completions`).
2673
+ *
2674
+ * Driven entirely by the {@link ModelCapabilities.requiresResponsesApiForTools}
2675
+ * flag in the model registry — there are no hardcoded model-name checks here.
2676
+ * Unknown models and models without the flag return `false` (stay on Chat
2677
+ * Completions).
2678
+ *
2679
+ * @param model The model to check.
2680
+ * @returns True when tool-bearing calls for this model must be routed to the
2681
+ * Responses API, false otherwise.
2682
+ */
2683
+ function requiresResponsesApiForTools(model) {
2684
+ if (!isValidModel(model)) {
2685
+ return false;
2686
+ }
2687
+ return getModelCapabilities(model).requiresResponsesApiForTools ?? false;
2688
+ }
2689
+ /**
2690
+ * OpenAI's `reasoning_effort` ladder, ordered from least to most effort. This is
2691
+ * the full 2026 ladder (developers.openai.com), including `'max'` — the OpenAI
2692
+ * SDK v6 `Shared.ReasoningEffort` type accepts this entire set on the wire.
2693
+ * Mirrors the widened {@link LLMOptions.reasoning_effort} union.
2694
+ */
2695
+ const REASONING_EFFORT_LADDER = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
2696
+ /**
2697
+ * Per-model `reasoning_effort` support window for OpenAI reasoning models,
2698
+ * expressed on {@link REASONING_EFFORT_LADDER}. `min`/`max` are the lowest and
2699
+ * highest effort each model's API accepts; a request below `min` clamps up and
2700
+ * above `max` clamps down, so an over/under-shoot degrades gracefully instead of
2701
+ * returning HTTP 400.
2702
+ *
2703
+ * Reasoning models ABSENT from this table omit `reasoning_effort` entirely —
2704
+ * notably `o1-mini`, which does not accept the parameter at all. The windows
2705
+ * record TRUE API support; the wire value is additionally narrowed to
2706
+ * {@link SdkReasoningEffort} in {@link toSdkReasoningEffort}, so when the SDK
2707
+ * widens only that mapping (not these windows) needs revisiting.
2708
+ */
2709
+ const MODEL_REASONING_EFFORT_SUPPORT = {
2710
+ // GPT-5.6 reasoning trio — full ladder (developers.openai.com/api/docs/models).
2711
+ 'gpt-5.6-sol': { min: 'minimal', max: 'xhigh' },
2712
+ 'gpt-5.6-terra': { min: 'minimal', max: 'xhigh' },
2713
+ 'gpt-5.6-luna': { min: 'minimal', max: 'xhigh' },
2714
+ // o-series reasoning models accept low|medium|high (no none/minimal/xhigh).
2715
+ o1: { min: 'low', max: 'high' },
2716
+ o3: { min: 'low', max: 'high' },
2717
+ 'o3-mini': { min: 'low', max: 'high' },
2718
+ 'o4-mini': { min: 'low', max: 'high' },
2719
+ // o1-mini is intentionally omitted — it rejects reasoning_effort outright.
2720
+ };
2721
+ /**
2722
+ * Identity pass-through from a full-ladder effort level to an SDK-sendable value.
2723
+ * Under OpenAI SDK v6 the wire type ({@link SdkReasoningEffort}) spans the entire
2724
+ * ladder, so every level is sendable as-is; the per-model
2725
+ * {@link MODEL_REASONING_EFFORT_SUPPORT} window (applied in
2726
+ * {@link clampReasoningEffort}) is what keeps a request within a model's range.
2727
+ * Retained as a named seam so a future SDK narrowing has one obvious place to
2728
+ * re-introduce folding.
2729
+ *
2730
+ * @param level A level on {@link REASONING_EFFORT_LADDER}.
2731
+ * @returns The same level, typed as SDK-sendable.
2732
+ */
2733
+ function toSdkReasoningEffort(level) {
2734
+ return level;
2735
+ }
2736
+ /**
2737
+ * Clamps a requested {@link LLMOptions.reasoning_effort} to a value the target
2738
+ * model — and the installed OpenAI SDK — will accept, degrading gracefully so an
2739
+ * unsupported (too-high or too-low) level never returns HTTP 400.
2740
+ *
2741
+ * Resolution:
2742
+ * 1. Models without an entry in {@link MODEL_REASONING_EFFORT_SUPPORT}
2743
+ * (non-reasoning models, or reasoning models like `o1-mini` that reject the
2744
+ * parameter) ⇒ `undefined`, i.e. omit `reasoning_effort` from the request.
2745
+ * 2. Clamp the requested level into the model's `[min, max]` window.
2746
+ * 3. Fold onto the SDK-sendable range via {@link toSdkReasoningEffort}.
2747
+ *
2748
+ * Shared by BOTH OpenAI transports so the Chat and Responses paths clamp
2749
+ * identically.
2750
+ *
2751
+ * @param requested The caller's requested effort (full ladder).
2752
+ * @param model The normalized model name the request targets.
2753
+ * @returns An SDK-safe effort, or `undefined` when the field must be omitted.
2754
+ */
2755
+ function clampReasoningEffort(requested, model) {
2756
+ const support = MODEL_REASONING_EFFORT_SUPPORT[model];
2757
+ if (!support) {
2758
+ return undefined;
2759
+ }
2760
+ const requestedIdx = REASONING_EFFORT_LADDER.indexOf(requested);
2761
+ if (requestedIdx < 0) {
2762
+ return undefined;
2763
+ }
2764
+ const minIdx = REASONING_EFFORT_LADDER.indexOf(support.min);
2765
+ const maxIdx = REASONING_EFFORT_LADDER.indexOf(support.max);
2766
+ const clampedIdx = Math.min(Math.max(requestedIdx, minIdx), maxIdx);
2767
+ return toSdkReasoningEffort(REASONING_EFFORT_LADDER[clampedIdx]);
2768
+ }
2601
2769
  /**
2602
2770
  * Normalizes the response format option based on model compatibility.
2603
2771
  * @param responseFormat The desired response format.
@@ -2729,6 +2897,18 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
2729
2897
  if (options.max_completion_tokens !== undefined) {
2730
2898
  queryOptions.max_completion_tokens = options.max_completion_tokens;
2731
2899
  }
2900
+ // Forward reasoning_effort on the Chat path for reasoning models. Chat
2901
+ // Completions accepts reasoning_effort for reasoning models (only tools +
2902
+ // effort together 400 on GPT-5.6, and those tool-bearing calls are already
2903
+ // routed to the Responses API), so this is safe. The value is clamped per
2904
+ // model so an unsupported level degrades gracefully instead of 400-ing;
2905
+ // clampReasoningEffort returns undefined for models that reject the parameter.
2906
+ if (options.reasoning_effort !== undefined && isReasoningModel(normalizedModel)) {
2907
+ const effort = clampReasoningEffort(options.reasoning_effort, normalizedModel);
2908
+ if (effort !== undefined) {
2909
+ queryOptions.reasoning_effort = effort;
2910
+ }
2911
+ }
2732
2912
  // Only set response_format when it's not the default 'text' type
2733
2913
  // (sending response_format: {type: 'text'} is redundant and may cause issues)
2734
2914
  const responseFormatOption = getResponseFormatOption(responseFormat, normalizedModel);
@@ -2798,12 +2978,18 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
2798
2978
  const response = {
2799
2979
  id: completion.id,
2800
2980
  content: completion.choices[0]?.message?.content || '',
2801
- tool_calls: completion.choices[0]?.message?.tool_calls,
2981
+ // OpenAI SDK v6 widened tool calls into a function/custom union; the engine
2982
+ // only emits + parses function calls, so narrow to the function variant to
2983
+ // preserve the byte-identical `{ id, type, function }` shape.
2984
+ tool_calls: completion.choices[0]?.message?.tool_calls?.filter((tc) => tc.type === 'function'),
2802
2985
  usage: {
2803
2986
  prompt_tokens: completion.usage?.prompt_tokens ?? 0,
2804
2987
  completion_tokens: completion.usage?.completion_tokens ?? 0,
2805
2988
  total_tokens: completion.usage?.total_tokens ?? 0,
2806
2989
  cached_tokens: cachedTokens,
2990
+ // Non-reasoning Chat-Completions models report no reasoning tokens; read
2991
+ // defensively so the field is always present and correct.
2992
+ reasoning_tokens: completion.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
2807
2993
  },
2808
2994
  system_fingerprint: completion.system_fingerprint,
2809
2995
  service_tier: options.service_tier,
@@ -2812,9 +2998,383 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
2812
2998
  };
2813
2999
  return response;
2814
3000
  }
3001
+ // ─────────────────────────────────────────────────────────────────────────────
3002
+ // Transparent Chat-Completions → Responses API routing (GPT-5.6 reasoning trio)
3003
+ //
3004
+ // OpenAI's GPT-5.6 reasoning models (sol/terra/luna) return HTTP 400 for
3005
+ // function tools on `/v1/chat/completions`
3006
+ // ("Function tools with reasoning_effort are not supported for gpt-5.6-sol in
3007
+ // /v1/chat/completions"); they require the Responses API (`/v1/responses`).
3008
+ // The helpers below convert a Chat-Completions-style request into a Responses
3009
+ // request and normalize the Responses result back into the SAME internal
3010
+ // {@link CompletionResponse}, so callers of {@link makeOpenAIChatCompletionCall}
3011
+ // receive a byte-identical {@link LLMResponse} envelope regardless of which
3012
+ // endpoint served the call. The routing decision is data-driven via
3013
+ // {@link requiresResponsesApiForTools}.
3014
+ // ─────────────────────────────────────────────────────────────────────────────
3015
+ /**
3016
+ * Coerces a Chat-Completions message `content` value (string, content-part
3017
+ * array, or null) into a plain string. Mirrors the text-extraction rule used
3018
+ * elsewhere in this module: array parts contribute their `text` field, all
3019
+ * other shapes contribute nothing.
3020
+ *
3021
+ * @param content The message content in any of its Chat-Completions shapes.
3022
+ * @returns The concatenated text, or an empty string when there is none.
3023
+ */
3024
+ function coerceMessageText(content) {
3025
+ if (typeof content === 'string') {
3026
+ return content;
3027
+ }
3028
+ if (Array.isArray(content)) {
3029
+ return content
3030
+ .map((part) => typeof part === 'object' && part !== null && 'text' in part &&
3031
+ typeof part.text === 'string'
3032
+ ? part.text
3033
+ : '')
3034
+ .join('');
3035
+ }
3036
+ return '';
3037
+ }
3038
+ /**
3039
+ * Converts Chat-Completions user content (string or multi-modal content parts)
3040
+ * into the Responses API content shape. String content passes through
3041
+ * unchanged; content-part arrays map `text` → `input_text` and `image_url` →
3042
+ * `input_image`. Unsupported part kinds (audio/file) are skipped rather than
3043
+ * emitted in an incompatible shape — no tool-calling caller uses them today.
3044
+ *
3045
+ * @param content The Chat-Completions user content.
3046
+ * @returns A string or an array of Responses input-content parts.
3047
+ */
3048
+ function chatContentToResponsesContent(content) {
3049
+ if (typeof content === 'string') {
3050
+ return content;
3051
+ }
3052
+ const parts = [];
3053
+ for (const part of content) {
3054
+ if (part.type === 'text') {
3055
+ parts.push({ type: 'input_text', text: part.text });
3056
+ }
3057
+ else if (part.type === 'image_url') {
3058
+ parts.push({
3059
+ type: 'input_image',
3060
+ image_url: part.image_url.url,
3061
+ detail: part.image_url.detail ?? 'auto',
3062
+ });
3063
+ }
3064
+ }
3065
+ return parts;
3066
+ }
3067
+ /**
3068
+ * Converts a single Chat-Completions message into zero or more Responses API
3069
+ * input items. Assistant tool calls become `function_call` items and `tool`
3070
+ * results become `function_call_output` items — both keyed by the SAME id the
3071
+ * chat envelope exposed as `tool_call.id` / `tool_call_id`, so a multi-turn
3072
+ * tool conversation round-trips correctly through the Responses transport.
3073
+ *
3074
+ * @param message The Chat-Completions message to convert.
3075
+ * @returns The equivalent Responses input items (may be empty).
3076
+ */
3077
+ function chatMessageToResponsesInput(message) {
3078
+ switch (message.role) {
3079
+ case 'system':
3080
+ return [{ role: 'system', content: coerceMessageText(message.content) }];
3081
+ case 'developer':
3082
+ return [{ role: 'developer', content: coerceMessageText(message.content) }];
3083
+ case 'user':
3084
+ return [{ role: 'user', content: chatContentToResponsesContent(message.content) }];
3085
+ case 'assistant': {
3086
+ const items = [];
3087
+ const text = coerceMessageText(message.content);
3088
+ if (text.length > 0) {
3089
+ items.push({ role: 'assistant', content: text });
3090
+ }
3091
+ if (message.tool_calls) {
3092
+ for (const toolCall of message.tool_calls) {
3093
+ if (toolCall.type === 'function') {
3094
+ items.push({
3095
+ type: 'function_call',
3096
+ call_id: toolCall.id,
3097
+ name: toolCall.function.name,
3098
+ arguments: toolCall.function.arguments,
3099
+ });
3100
+ }
3101
+ }
3102
+ }
3103
+ return items;
3104
+ }
3105
+ case 'tool':
3106
+ return [{
3107
+ type: 'function_call_output',
3108
+ call_id: message.tool_call_id,
3109
+ output: coerceMessageText(message.content),
3110
+ }];
3111
+ default:
3112
+ // Deprecated `function`-role messages carry no call_id to correlate; keep
3113
+ // the text so history stays coherent. No current caller emits these.
3114
+ return [{ role: 'assistant', content: coerceMessageText(message.content) }];
3115
+ }
3116
+ }
3117
+ /**
3118
+ * Converts a Chat-Completions function tool (`{ type:'function', function:{…} }`)
3119
+ * into a Responses API function tool (`{ type:'function', name, parameters,
3120
+ * strict, description }`). `strict` defaults to `false` to preserve Chat
3121
+ * Completions' effective default (non-strict) — the Responses API otherwise
3122
+ * defaults function tools to strict, which would silently change model
3123
+ * behaviour and could reject payloads the Chat path accepted.
3124
+ *
3125
+ * @param tool The Chat-Completions function tool.
3126
+ * @returns The equivalent Responses API function tool.
3127
+ */
3128
+ function chatToolToResponsesFunctionTool(tool) {
3129
+ return {
3130
+ type: 'function',
3131
+ name: tool.function.name,
3132
+ description: tool.function.description,
3133
+ parameters: tool.function.parameters ?? {},
3134
+ strict: tool.function.strict ?? false,
3135
+ };
3136
+ }
3137
+ /**
3138
+ * Maps an {@link OpenAIResponseFormat} onto the Responses API `text.format`
3139
+ * config, applying the same model-capability gates as the Chat path
3140
+ * ({@link getResponseFormatOption}): `'text'` yields `undefined` (omit
3141
+ * `text.format`), `'json'` yields `json_object` only when the model supports
3142
+ * JSON mode, and a `json_schema` request yields the flat Responses
3143
+ * `json_schema` format (name/schema at the top level, unlike Chat's nested
3144
+ * `json_schema` wrapper).
3145
+ *
3146
+ * @param responseFormat The requested response format.
3147
+ * @param normalizedModel The normalized model name.
3148
+ * @returns The `text.format` config, or `undefined` when none should be sent.
3149
+ * @throws Error when a `json_schema` format is requested for an incompatible model.
3150
+ */
3151
+ function getResponsesTextFormat(responseFormat, normalizedModel) {
3152
+ if (responseFormat === 'text') {
3153
+ return undefined;
3154
+ }
3155
+ if (responseFormat === 'json') {
3156
+ return supportsJsonMode(normalizedModel) ? { type: 'json_object' } : undefined;
3157
+ }
3158
+ if (typeof responseFormat === 'object' && responseFormat.type === 'json_schema') {
3159
+ if (!isStructuredOutputCompatibleModel(normalizedModel)) {
3160
+ throw new Error(`${normalizedModel} is not compatible with structured outputs / json object.`);
3161
+ }
3162
+ return {
3163
+ type: 'json_schema',
3164
+ name: 'Response',
3165
+ schema: {
3166
+ type: 'object',
3167
+ properties: responseFormat.schema.properties,
3168
+ required: responseFormat.schema.required || [],
3169
+ },
3170
+ };
3171
+ }
3172
+ throw new Error(`Invalid response format: ${String(responseFormat)}`);
3173
+ }
3174
+ /**
3175
+ * Builds an OpenAI Responses API request body from the exact same arguments a
3176
+ * caller passes to {@link makeOpenAIChatCompletionCall}. This is the request
3177
+ * half of the transparent routing layer.
3178
+ *
3179
+ * The mapping deliberately mirrors {@link createCompletion}'s Chat request
3180
+ * one-for-one so request semantics are preserved across transports:
3181
+ * - `developerPrompt` (+ context messages + user `content`) → `input` items
3182
+ * (chat `tool_calls`/`tool` messages become `function_call`/
3183
+ * `function_call_output` items).
3184
+ * - chat `tools` → Responses `tools` (function tools only).
3185
+ * - `temperature` → `temperature`, gated by {@link supportsTemperature} exactly
3186
+ * as the Chat path (dropped for GPT-5.6/o-series reasoning models).
3187
+ * - `max_completion_tokens` → `max_output_tokens`.
3188
+ * - {@link OpenAIResponseFormat} → `text.format`.
3189
+ * - `reasoning_effort` → `reasoning.effort`, clamped per-model via
3190
+ * {@link clampReasoningEffort} and gated on {@link isReasoningModel} exactly as
3191
+ * the Chat path, so the two transports clamp identically. Only set when the
3192
+ * caller explicitly supplies it — parity is preserved for callers (like the
3193
+ * engine decision path) that do not.
3194
+ *
3195
+ * Parameters the Chat path does not forward (`top_p`, `parallel_tool_calls`,
3196
+ * `tool_choice`, `metadata`, `store`) are intentionally omitted here too, so the
3197
+ * request semantics stay identical to today's production Chat behaviour.
3198
+ *
3199
+ * @param content The user content (string or multi-modal content parts).
3200
+ * @param responseFormat The desired response format.
3201
+ * @param options The Chat-Completions-style options bag.
3202
+ * @param normalizedModel The normalized model name to target.
3203
+ * @returns A ready-to-send non-streaming Responses API request body.
3204
+ * @throws Error when a `json_schema` format is requested for an incompatible model.
3205
+ */
3206
+ function buildResponsesRequestFromChatOptions(content, responseFormat, options, normalizedModel) {
3207
+ const input = [];
3208
+ if (options.developerPrompt && supportsDeveloperPrompt(normalizedModel)) {
3209
+ input.push({ role: 'developer', content: options.developerPrompt });
3210
+ }
3211
+ if (options.context) {
3212
+ for (const message of options.context) {
3213
+ input.push(...chatMessageToResponsesInput(message));
3214
+ }
3215
+ }
3216
+ input.push({ role: 'user', content: chatContentToResponsesContent(content) });
3217
+ const requestBody = {
3218
+ model: normalizedModel,
3219
+ input,
3220
+ };
3221
+ if (options.tools && options.tools.length > 0) {
3222
+ const functionTools = options.tools
3223
+ .filter((tool) => tool.type === 'function')
3224
+ .map(chatToolToResponsesFunctionTool);
3225
+ if (functionTools.length > 0) {
3226
+ requestBody.tools = functionTools;
3227
+ }
3228
+ }
3229
+ if (options.temperature !== undefined && supportsTemperature(normalizedModel)) {
3230
+ requestBody.temperature = options.temperature;
3231
+ }
3232
+ if (options.max_completion_tokens !== undefined) {
3233
+ requestBody.max_output_tokens = options.max_completion_tokens;
3234
+ }
3235
+ const textFormat = getResponsesTextFormat(responseFormat, normalizedModel);
3236
+ if (textFormat) {
3237
+ requestBody.text = { format: textFormat };
3238
+ }
3239
+ if (options.reasoning_effort !== undefined && isReasoningModel(normalizedModel)) {
3240
+ const effort = clampReasoningEffort(options.reasoning_effort, normalizedModel);
3241
+ if (effort !== undefined) {
3242
+ requestBody.reasoning = { effort };
3243
+ }
3244
+ }
3245
+ return requestBody;
3246
+ }
3247
+ /**
3248
+ * Normalizes an OpenAI Responses API result into the internal
3249
+ * {@link CompletionResponse} shape. This is the response half of the
3250
+ * transparent routing layer and is the single most correctness-critical mapping
3251
+ * in this module: the returned `tool_calls` MUST be byte-identical to what the
3252
+ * Chat path produces so downstream consumers (notably the engine decision path,
3253
+ * which reads `tool_call.function.arguments` as a JSON string) parse it
3254
+ * unchanged.
3255
+ *
3256
+ * Mapping:
3257
+ * - `function_call` output items → `{ id: call_id, type:'function',
3258
+ * function:{ name, arguments } }` with `arguments` kept as the raw JSON
3259
+ * string (never pre-parsed) — exactly the Chat `ChatCompletionMessageToolCall`
3260
+ * shape. `id` is sourced from `call_id` (not the item `id`) so it round-trips
3261
+ * as the `tool_call_id` in a multi-turn conversation.
3262
+ * - `message` output items → concatenated `output_text` parts (refusals and
3263
+ * non-text parts contribute nothing, matching the Chat path's null-content →
3264
+ * empty-string behaviour).
3265
+ * - usage: `input_tokens`→prompt, `output_tokens`→completion,
3266
+ * `input_tokens_details.cached_tokens`→cached,
3267
+ * `output_tokens_details.reasoning_tokens`→reasoning.
3268
+ *
3269
+ * `system_fingerprint` is `undefined` — the Responses API does not return one
3270
+ * (the Chat path forwards it; consumers already treat it as optional).
3271
+ *
3272
+ * @param response The raw OpenAI Responses API result.
3273
+ * @param normalizedModel The normalized model name the request targeted.
3274
+ * @param serviceTier The caller's requested service tier, echoed through as on
3275
+ * the Chat path.
3276
+ * @returns A {@link CompletionResponse} structurally identical to the Chat path's.
3277
+ */
3278
+ function normalizeResponsesToCompletion(response, normalizedModel, serviceTier) {
3279
+ const toolCalls = response.output
3280
+ ?.filter((item) => item.type === 'function_call')
3281
+ .map((toolCall) => ({
3282
+ id: toolCall.call_id,
3283
+ type: 'function',
3284
+ function: {
3285
+ name: toolCall.name,
3286
+ arguments: toolCall.arguments,
3287
+ },
3288
+ }));
3289
+ const content = response.output
3290
+ ?.filter((item) => item.type === 'message')
3291
+ .map((item) => item.content
3292
+ .filter((part) => part.type === 'output_text')
3293
+ .map((part) => part.text)
3294
+ .join(''))
3295
+ .join('') || '';
3296
+ return {
3297
+ id: response.id,
3298
+ content,
3299
+ tool_calls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined,
3300
+ usage: {
3301
+ prompt_tokens: response.usage?.input_tokens ?? 0,
3302
+ completion_tokens: response.usage?.output_tokens ?? 0,
3303
+ total_tokens: response.usage?.total_tokens ?? 0,
3304
+ cached_tokens: response.usage?.input_tokens_details?.cached_tokens ?? 0,
3305
+ reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens ?? 0,
3306
+ },
3307
+ system_fingerprint: undefined,
3308
+ service_tier: serviceTier,
3309
+ provider: 'openai',
3310
+ model: normalizedModel,
3311
+ };
3312
+ }
3313
+ /**
3314
+ * Executes an OpenAI Responses API call for a Chat-Completions-style request and
3315
+ * returns the internal {@link CompletionResponse}. Mirrors {@link createCompletion}'s
3316
+ * transport concerns exactly — API-key resolution, timeout, `AbortSignal`
3317
+ * plumbing, the shared retry policy, and metadata-only failure logging — so the
3318
+ * two producers differ ONLY in the endpoint they hit.
3319
+ *
3320
+ * @param content The user content (string or multi-modal content parts).
3321
+ * @param responseFormat The desired response format.
3322
+ * @param options The Chat-Completions-style options bag.
3323
+ * @returns The normalized completion, identical in shape to the Chat path.
3324
+ * @throws Error when the API key is missing or the API call ultimately fails.
3325
+ */
3326
+ async function createResponsesCompletion(content, responseFormat, options = DEFAULT_OPTIONS$1) {
3327
+ const normalizedModel = normalizeModelName(options.model || DEFAULT_MODEL);
3328
+ const apiKey = options.apiKey || getSecrets().openai.apiKey;
3329
+ if (!apiKey) {
3330
+ throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
3331
+ }
3332
+ const openai = new OpenAI({
3333
+ apiKey: apiKey,
3334
+ timeout: options.timeout ?? LLM_TIMEOUT_MS,
3335
+ });
3336
+ const requestBody = buildResponsesRequestFromChatOptions(content, responseFormat, options, normalizedModel);
3337
+ let response;
3338
+ try {
3339
+ response = await withRetry(
3340
+ // Same AbortSignal plumbing as the Chat path — a fired signal tears down
3341
+ // the in-flight Responses request and releases its body buffer.
3342
+ () => openai.responses.create(requestBody, options.signal ? { signal: options.signal } : undefined), {
3343
+ maxRetries: 3,
3344
+ baseDelayMs: 2000,
3345
+ maxDelayMs: 30000,
3346
+ retryableErrors: isRetryableLLMError,
3347
+ signal: options.signal,
3348
+ }, `OpenAI-Responses:${normalizedModel}`);
3349
+ }
3350
+ catch (error) {
3351
+ // Metadata-only failure snapshot (no message content, no API key), matching
3352
+ // the Chat path's defensive observability.
3353
+ const errorMessage = error instanceof Error ? error.message : String(error);
3354
+ getLumicLogger().error(`OpenAI Responses call failed for model ${normalizedModel}`, {
3355
+ model: normalizedModel,
3356
+ errorMessage,
3357
+ inputItemCount: Array.isArray(requestBody.input) ? requestBody.input.length : 1,
3358
+ toolCount: requestBody.tools?.length ?? 0,
3359
+ hasTemperature: requestBody.temperature !== undefined && requestBody.temperature !== null,
3360
+ hasTextFormat: requestBody.text?.format !== undefined,
3361
+ hasMaxOutputTokens: requestBody.max_output_tokens !== undefined && requestBody.max_output_tokens !== null,
3362
+ hasReasoningEffort: requestBody.reasoning?.effort !== undefined && requestBody.reasoning?.effort !== null,
3363
+ });
3364
+ throw error;
3365
+ }
3366
+ return normalizeResponsesToCompletion(response, normalizedModel, options.service_tier);
3367
+ }
2815
3368
  /**
2816
3369
  * Makes a call to the Lumic LLM interface, either to the default model or to one specified.
2817
3370
  *
3371
+ * Transport is chosen transparently: models flagged
3372
+ * {@link ModelCapabilities.requiresResponsesApiForTools} (the GPT-5.6 reasoning
3373
+ * trio) that are called WITH function tools are routed to the OpenAI Responses
3374
+ * API; every other call stays on Chat Completions. The returned
3375
+ * {@link LLMResponse} envelope — `response`, `tool_calls`, and `usage` — is
3376
+ * byte-identical across both transports, so callers require no changes.
3377
+ *
2818
3378
  * @param content The content to pass to the LLM.
2819
3379
  * @param responseFormat The format of the response. Defaults to 'text'.
2820
3380
  * @param options The options for the LLM call. Defaults to an empty object.
@@ -2825,11 +3385,28 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = 'text', op
2825
3385
  ...DEFAULT_OPTIONS$1,
2826
3386
  ...options,
2827
3387
  };
2828
- const completion = await createCompletion(content, responseFormat, mergedOptions);
3388
+ // Transparent transport routing. GPT-5.6 reasoning models 400 on
3389
+ // Chat-Completions + function tools, so route tool-bearing calls for such
3390
+ // models to the Responses API. `createResponsesCompletion` returns the SAME
3391
+ // CompletionResponse shape, so all cost-tracking and envelope construction
3392
+ // below is shared and the caller-facing result is identical.
3393
+ const normalizedModel = normalizeModelName(mergedOptions.model || DEFAULT_MODEL);
3394
+ const hasTools = !!(mergedOptions.tools && mergedOptions.tools.length > 0);
3395
+ const completion = requiresResponsesApiForTools(normalizedModel) && hasTools
3396
+ ? await createResponsesCompletion(content, responseFormat, mergedOptions)
3397
+ : await createCompletion(content, responseFormat, mergedOptions);
2829
3398
  // Track cost in the global cost tracker. Pass cached tokens through so the
2830
3399
  // tracker applies the discounted cached-input rate (typically ~50% of the
2831
3400
  // standard input rate) instead of billing every input token at full price.
2832
- getLLMCostTracker().trackUsage('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens);
3401
+ // reasoning_tokens is 0 on the Chat path and the real count on the Responses
3402
+ // path, so reasoning-heavy models are billed accurately.
3403
+ getLLMCostTracker().trackUsage('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, completion.usage.reasoning_tokens, completion.usage.cached_tokens);
3404
+ // Reasoning tokens are a SUBSET of completion (output) tokens. `calculateCost`
3405
+ // bills output AND reasoning at the output rate, so passing the full
3406
+ // completion_tokens alongside reasoning_tokens would bill reasoning twice.
3407
+ // Cost the non-reasoning remainder here and let calculateCost add reasoning
3408
+ // back once; the reported usage below keeps the full completion_tokens.
3409
+ const nonReasoningOutputTokens = completion.usage.completion_tokens - completion.usage.reasoning_tokens;
2833
3410
  // Handle tool calls differently
2834
3411
  if (completion.tool_calls && completion.tool_calls.length > 0) {
2835
3412
  const toolCallResponse = {
@@ -2844,11 +3421,11 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = 'text', op
2844
3421
  usage: {
2845
3422
  prompt_tokens: completion.usage.prompt_tokens,
2846
3423
  completion_tokens: completion.usage.completion_tokens,
2847
- reasoning_tokens: 0,
3424
+ reasoning_tokens: completion.usage.reasoning_tokens,
2848
3425
  provider: 'openai',
2849
3426
  model: completion.model,
2850
3427
  cached_tokens: completion.usage.cached_tokens,
2851
- cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
3428
+ cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, nonReasoningOutputTokens, completion.usage.reasoning_tokens, completion.usage.cached_tokens),
2852
3429
  },
2853
3430
  tool_calls: completion.tool_calls,
2854
3431
  };
@@ -2863,11 +3440,11 @@ const makeOpenAIChatCompletionCall = async (content, responseFormat = 'text', op
2863
3440
  usage: {
2864
3441
  prompt_tokens: completion.usage.prompt_tokens,
2865
3442
  completion_tokens: completion.usage.completion_tokens,
2866
- reasoning_tokens: 0,
3443
+ reasoning_tokens: completion.usage.reasoning_tokens,
2867
3444
  provider: 'openai',
2868
3445
  model: completion.model,
2869
3446
  cached_tokens: completion.usage.cached_tokens,
2870
- cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, completion.usage.completion_tokens, 0, completion.usage.cached_tokens),
3447
+ cost: calculateCost('openai', completion.model, completion.usage.prompt_tokens, nonReasoningOutputTokens, completion.usage.reasoning_tokens, completion.usage.cached_tokens),
2871
3448
  },
2872
3449
  tool_calls: completion.tool_calls,
2873
3450
  };
@@ -2926,8 +3503,14 @@ const makeResponsesAPICall = async (input, options = {}) => {
2926
3503
  // Responses API exposes cached input tokens under `input_tokens_details.cached_tokens`
2927
3504
  // (the equivalent of Chat Completions' `prompt_tokens_details.cached_tokens`).
2928
3505
  const responsesCachedTokens = response.usage?.input_tokens_details?.cached_tokens || 0;
3506
+ const responsesOutputTokens = response.usage?.output_tokens || 0;
3507
+ const responsesReasoningTokens = response.usage?.output_tokens_details?.reasoning_tokens || 0;
3508
+ // Reasoning tokens are a SUBSET of output tokens; cost the non-reasoning
3509
+ // remainder so calculateCost does not bill reasoning twice (same rationale as
3510
+ // makeOpenAIChatCompletionCall). Reported usage keeps the full output/reasoning counts.
3511
+ const responsesNonReasoningOutputTokens = responsesOutputTokens - responsesReasoningTokens;
2929
3512
  // Track cost in the global cost tracker
2930
- getLLMCostTracker().trackUsage('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens);
3513
+ getLLMCostTracker().trackUsage('openai', normalizedModel, response.usage?.input_tokens || 0, responsesOutputTokens, responsesReasoningTokens, responsesCachedTokens);
2931
3514
  // Extract tool calls from the output
2932
3515
  const toolCalls = response.output
2933
3516
  ?.filter((item) => item.type === 'function_call')
@@ -2964,12 +3547,12 @@ const makeResponsesAPICall = async (input, options = {}) => {
2964
3547
  response: toolCallResponse,
2965
3548
  usage: {
2966
3549
  prompt_tokens: response.usage?.input_tokens || 0,
2967
- completion_tokens: response.usage?.output_tokens || 0,
2968
- reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
3550
+ completion_tokens: responsesOutputTokens,
3551
+ reasoning_tokens: responsesReasoningTokens,
2969
3552
  provider: 'openai',
2970
3553
  model: normalizedModel,
2971
3554
  cached_tokens: responsesCachedTokens,
2972
- cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
3555
+ cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, responsesNonReasoningOutputTokens, responsesReasoningTokens, responsesCachedTokens),
2973
3556
  },
2974
3557
  tool_calls: toolCalls,
2975
3558
  ...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
@@ -2997,12 +3580,12 @@ const makeResponsesAPICall = async (input, options = {}) => {
2997
3580
  response: parsedResponse,
2998
3581
  usage: {
2999
3582
  prompt_tokens: response.usage?.input_tokens || 0,
3000
- completion_tokens: response.usage?.output_tokens || 0,
3001
- reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0,
3583
+ completion_tokens: responsesOutputTokens,
3584
+ reasoning_tokens: responsesReasoningTokens,
3002
3585
  provider: 'openai',
3003
3586
  model: normalizedModel,
3004
3587
  cached_tokens: responsesCachedTokens,
3005
- cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, response.usage?.output_tokens || 0, response.usage?.output_tokens_details?.reasoning_tokens || 0, responsesCachedTokens),
3588
+ cost: calculateCost('openai', normalizedModel, response.usage?.input_tokens || 0, responsesNonReasoningOutputTokens, responsesReasoningTokens, responsesCachedTokens),
3006
3589
  },
3007
3590
  tool_calls: toolCalls,
3008
3591
  ...(codeInterpreterOutputs ? { code_interpreter_outputs: codeInterpreterOutputs } : {}),
@@ -8333,7 +8916,12 @@ const isRetryableAnthropicError = (error) => {
8333
8916
  * Anthropic: { name, description, input_schema }
8334
8917
  */
8335
8918
  function translateToolsToAnthropic(tools) {
8336
- return tools.map((tool) => ({
8919
+ // OpenAI SDK v6 widened `ChatCompletionTool` into a function/custom union.
8920
+ // lumic-utils only supports function tools, so narrow to the function variant
8921
+ // before reading `.function`. Custom tools (which lumic never emits) are skipped.
8922
+ return tools
8923
+ .filter((tool) => tool.type === 'function')
8924
+ .map((tool) => ({
8337
8925
  name: tool.function.name,
8338
8926
  description: tool.function.description || '',
8339
8927
  input_schema: (tool.function.parameters || { type: 'object', properties: {} }),
@@ -8727,8 +9315,11 @@ async function makeOpenAICompatibleCall(content, responseFormat = 'text', option
8727
9315
  const completionTokens = completion.usage?.completion_tokens || 0;
8728
9316
  // Track cost
8729
9317
  getLLMCostTracker().trackUsage(providerConfig.name, model, promptTokens, completionTokens);
8730
- // Handle tool calls
8731
- const toolCalls = completion.choices[0]?.message?.tool_calls;
9318
+ // Handle tool calls. OpenAI SDK v6 widened tool calls into a function/custom
9319
+ // union; these OpenAI-compatible providers only return function calls, so
9320
+ // narrow to the function variant to preserve the byte-identical
9321
+ // `function.{name,arguments}` shape the engine parses.
9322
+ const toolCalls = completion.choices[0]?.message?.tool_calls?.filter((tc) => tc.type === 'function');
8732
9323
  if (toolCalls && toolCalls.length > 0) {
8733
9324
  const toolCallResponse = {
8734
9325
  tool_calls: toolCalls.map((tc) => ({
@@ -9277,7 +9868,13 @@ const makeDeepseekCall = async (content, responseFormat = 'json', options = {})
9277
9868
  // Handle tool calls similarly to OpenAI
9278
9869
  if (completion.tool_calls && completion.tool_calls.length > 0) {
9279
9870
  const toolCallResponse = {
9280
- tool_calls: completion.tool_calls.map((tc) => ({
9871
+ // OpenAI SDK v6 widened tool calls into a function/custom union. DeepSeek
9872
+ // (called via the OpenAI-compatible surface) only returns function calls,
9873
+ // so narrow to the function variant to preserve the byte-identical
9874
+ // `function.{name,arguments}` shape.
9875
+ tool_calls: completion.tool_calls
9876
+ .filter((tc) => tc.type === 'function')
9877
+ .map((tc) => ({
9281
9878
  id: tc.id,
9282
9879
  name: tc.function.name,
9283
9880
  arguments: JSON.parse(tc.function.arguments),
@@ -23231,11 +23828,11 @@ let poolConfig = DEFAULT_POOL_CONFIG;
23231
23828
  async function loadApolloModules() {
23232
23829
  if (typeof window === "undefined" || process.env.AWS_EXECUTION_ENV) {
23233
23830
  // Server-side (or Lambda): load the CommonJS‑based implementation.
23234
- return (await Promise.resolve().then(function () { return require('./apollo-client.server-DQPc4Iam.js'); }));
23831
+ return (await Promise.resolve().then(function () { return require('./apollo-client.server-DMz8cEcx.js'); }));
23235
23832
  }
23236
23833
  else {
23237
23834
  // Client-side: load the ESM‑based implementation.
23238
- return (await Promise.resolve().then(function () { return require('./apollo-client.client-CzTlfziA.js'); }));
23835
+ return (await Promise.resolve().then(function () { return require('./apollo-client.client-CNrz3Xod.js'); }));
23239
23836
  }
23240
23837
  }
23241
23838
  /**
@@ -80486,11 +81083,93 @@ const tools = [
80486
81083
  }
80487
81084
  }
80488
81085
  },
81086
+ {
81087
+ "type": "function",
81088
+ "function": {
81089
+ "name": "requiresResponsesApiForTools",
81090
+ "description": "Checks whether a model must use the OpenAI Responses API (`/v1/responses`) for function-tool calls rather than Chat Completions (`/v1/chat/completions`). Driven entirely by the {@link ModelCapabilities.requiresResponsesApiForTools} flag in the model registry — there are no hardcoded model-name checks here. Unknown models and models without the flag return `false` (stay on Chat Completions).",
81091
+ "parameters": {
81092
+ "type": "object",
81093
+ "properties": {
81094
+ "model": {
81095
+ "type": "string",
81096
+ "description": "The model to check."
81097
+ }
81098
+ },
81099
+ "required": [
81100
+ "model"
81101
+ ]
81102
+ }
81103
+ }
81104
+ },
81105
+ {
81106
+ "type": "function",
81107
+ "function": {
81108
+ "name": "buildResponsesRequestFromChatOptions",
81109
+ "description": "Builds an OpenAI Responses API request body from the exact same arguments a caller passes to {@link makeOpenAIChatCompletionCall}. This is the request half of the transparent routing layer. The mapping deliberately mirrors {@link createCompletion}'s Chat request one-for-one so request semantics are preserved across transports: - `developerPrompt` (+ context messages + user `content`) → `input` items (chat `tool_calls`/`tool` messages become `function_call`/ `function_call_output` items). - chat `tools` → Responses `tools` (function tools only). - `temperature` → `temperature`, gated by {@link supportsTemperature} exactly as the Chat path (dropped for GPT-5.6/o-series reasoning models). - `max_completion_tokens` → `max_output_tokens`. - {@link OpenAIResponseFormat} → `text.format`. - `reasoning_effort` → `reasoning.effort`, clamped per-model via {@link clampReasoningEffort} and gated on {@link isReasoningModel} exactly as the Chat path, so the two transports clamp identically. Only set when the caller explicitly supplies it — parity is preserved for callers (like the engine decision path) that do not. Parameters the Chat path does not forward (`top_p`, `parallel_tool_calls`, `tool_choice`, `metadata`, `store`) are intentionally omitted here too, so the request semantics stay identical to today's production Chat behaviour.",
81110
+ "parameters": {
81111
+ "type": "object",
81112
+ "properties": {
81113
+ "content": {
81114
+ "type": "string",
81115
+ "description": "The user content (string or multi-modal content parts)."
81116
+ },
81117
+ "responseFormat": {
81118
+ "type": "string",
81119
+ "description": "The desired response format."
81120
+ },
81121
+ "options": {
81122
+ "type": "string",
81123
+ "description": "The Chat-Completions-style options bag."
81124
+ },
81125
+ "normalizedModel": {
81126
+ "type": "string",
81127
+ "description": "The normalized model name to target."
81128
+ }
81129
+ },
81130
+ "required": [
81131
+ "content",
81132
+ "responseFormat",
81133
+ "options",
81134
+ "normalizedModel"
81135
+ ]
81136
+ }
81137
+ }
81138
+ },
81139
+ {
81140
+ "type": "function",
81141
+ "function": {
81142
+ "name": "normalizeResponsesToCompletion",
81143
+ "description": "Normalizes an OpenAI Responses API result into the internal {@link CompletionResponse} shape. This is the response half of the transparent routing layer and is the single most correctness-critical mapping in this module: the returned `tool_calls` MUST be byte-identical to what the Chat path produces so downstream consumers (notably the engine decision path, which reads `tool_call.function.arguments` as a JSON string) parse it unchanged. Mapping: - `function_call` output items → `{ id: call_id, type:'function', function:{ name, arguments } }` with `arguments` kept as the raw JSON string (never pre-parsed) — exactly the Chat `ChatCompletionMessageToolCall` shape. `id` is sourced from `call_id` (not the item `id`) so it round-trips as the `tool_call_id` in a multi-turn conversation. - `message` output items → concatenated `output_text` parts (refusals and non-text parts contribute nothing, matching the Chat path's null-content → empty-string behaviour). - usage: `input_tokens`→prompt, `output_tokens`→completion, `input_tokens_details.cached_tokens`→cached, `output_tokens_details.reasoning_tokens`→reasoning. `system_fingerprint` is `undefined` — the Responses API does not return one (the Chat path forwards it; consumers already treat it as optional).",
81144
+ "parameters": {
81145
+ "type": "object",
81146
+ "properties": {
81147
+ "response": {
81148
+ "type": "string",
81149
+ "description": "The raw OpenAI Responses API result."
81150
+ },
81151
+ "normalizedModel": {
81152
+ "type": "string",
81153
+ "description": "The normalized model name the request targeted."
81154
+ },
81155
+ "serviceTier": {
81156
+ "type": "string",
81157
+ "description": "The caller's requested service tier, echoed through as on the Chat path."
81158
+ }
81159
+ },
81160
+ "required": [
81161
+ "response",
81162
+ "normalizedModel",
81163
+ "serviceTier"
81164
+ ]
81165
+ }
81166
+ }
81167
+ },
80489
81168
  {
80490
81169
  "type": "function",
80491
81170
  "function": {
80492
81171
  "name": "makeOpenAIChatCompletionCall",
80493
- "description": "Makes a call to the Lumic LLM interface, either to the default model or to one specified.",
81172
+ "description": "Makes a call to the Lumic LLM interface, either to the default model or to one specified. Transport is chosen transparently: models flagged {@link ModelCapabilities.requiresResponsesApiForTools} (the GPT-5.6 reasoning trio) that are called WITH function tools are routed to the OpenAI Responses API; every other call stays on Chat Completions. The returned {@link LLMResponse} envelope — `response`, `tool_calls`, and `usage` — is byte-identical across both transports, so callers require no changes.",
80494
81173
  "parameters": {
80495
81174
  "type": "object",
80496
81175
  "properties": {
@@ -81789,9 +82468,11 @@ exports.JsonParseError = JsonParseError;
81789
82468
  exports.LLMCostTracker = LLMCostTracker;
81790
82469
  exports.LLMError = LLMError;
81791
82470
  exports.LLM_ADVANCED_PROVIDER = LLM_ADVANCED_PROVIDER;
82471
+ exports.LLM_CRITICAL_PROVIDER = LLM_CRITICAL_PROVIDER;
81792
82472
  exports.LLM_DEFAULT_PROVIDER = LLM_DEFAULT_PROVIDER;
81793
82473
  exports.LLM_MINI_PROVIDER = LLM_MINI_PROVIDER;
81794
82474
  exports.LLM_MODEL_ADVANCED = LLM_MODEL_ADVANCED;
82475
+ exports.LLM_MODEL_CRITICAL = LLM_MODEL_CRITICAL;
81795
82476
  exports.LLM_MODEL_MINI = LLM_MODEL_MINI;
81796
82477
  exports.LLM_MODEL_NORMAL = LLM_MODEL_NORMAL;
81797
82478
  exports.LLM_NORMAL_PROVIDER = LLM_NORMAL_PROVIDER;
@@ -81946,4 +82627,4 @@ exports.withCorrelationId = withCorrelationId;
81946
82627
  exports.withMetrics = withMetrics;
81947
82628
  exports.withRateLimit = withRateLimit;
81948
82629
  exports.withRetry = withRetry;
81949
- //# sourceMappingURL=index-FqzV71J4.js.map
82630
+ //# sourceMappingURL=index-DlAW691N.js.map