agent-accelerator 0.5.0 → 0.5.2

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.
@@ -18,9 +18,8 @@
18
18
  * (groq, ollama, …) fail unknown body properties, so anything unsupported
19
19
  * is warn+drop, never sent.
20
20
  * - `text`, `image_url`, and `input_audio` parts are sent; `video`/`file`
21
- * parts fail fast with a one-line error naming the remedy (files point at
22
- * `convertDocumentToMarkdown`). The old transport let these through to
23
- * confusing provider 400s.
21
+ * parts fail fast with a one-line error naming the remedy. The old transport
22
+ * let these through to confusing provider 400s.
24
23
  * - Prior-turn reasoning is NOT resent (chat history is messages + tool calls
25
24
  * only), except echoed Gemini thought signatures (see below).
26
25
  * - Gemini models served through OpenAI-compatible endpoints keep working:
@@ -81,7 +80,7 @@ import {
81
80
  } from "./shared.ts";
82
81
  import { getModelFromCatalog, createGenericModelSpec, modelSupportsStructuredOutput } from "../models/catalog.ts";
83
82
  import {
84
- applyCacheForCustom,
83
+ resolveCache,
85
84
  mapToolChoiceToOpenAI,
86
85
  emitProviderWarning,
87
86
  noteProviderTurn,
@@ -91,6 +90,7 @@ import {
91
90
  mapGenerationParamsToChat,
92
91
  normalizeStructuredOutput,
93
92
  augmentSystemPromptWithStructuredOutput,
93
+ warnIgnoredMediaResolution,
94
94
  } from "../providers.ts";
95
95
 
96
96
  // ---------------------------------------------------------------------------
@@ -182,13 +182,6 @@ interface ChatResponse {
182
182
  [k: string]: unknown;
183
183
  }
184
184
 
185
- /** Headers that must never leak onto native REST requests. */
186
- const INTERNAL_HEADERS = new Set([
187
- "x-thought-signature-map",
188
- "x-cached-content-id",
189
- "x-multimodal-user-content",
190
- ]);
191
-
192
185
  // ---------------------------------------------------------------------------
193
186
  // Request building (canonical -> Chat Completions)
194
187
  // ---------------------------------------------------------------------------
@@ -345,7 +338,7 @@ function assertNoVideoOrFileParts(
345
338
  const message =
346
339
  kind === "video"
347
340
  ? `[${prefix}/${modelId}] unsupported video input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
348
- : `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Convert the document to Markdown with the convert_document_to_markdown tool (or Agent bypassInputFileModality) and send it as text instead.`;
341
+ : `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a file-capable model (e.g. google/gemini-*) or convert the document to text before sending it.`;
349
342
  const err = new Error(message);
350
343
  err.name = "AgentAccelProviderError";
351
344
  Object.defineProperties(err, {
@@ -710,7 +703,9 @@ export class OpenAICompatibleChatProvider implements Provider {
710
703
  fallback: "standard routing (no service_tier payload is sent)",
711
704
  });
712
705
  }
713
- applyCacheForCustom(this.prefix, options?.cache, `${this.prefix}/${modelId}`);
706
+ // Strict endpoints reject unknown body fields: implicit caching only.
707
+ resolveCache(this.prefix, options?.cache, ["implicit"], `${this.prefix}/${modelId}`);
708
+ warnIgnoredMediaResolution(this.prefix, context.messages, `${this.prefix}/${modelId}`);
714
709
 
715
710
  // Structured outputs: native `response_format` when supported, else
716
711
  // emulated via system prompt (strict endpoints would 400 on json_schema
@@ -796,13 +791,9 @@ export class OpenAICompatibleChatProvider implements Provider {
796
791
  ): RequestInit {
797
792
  const headers: Record<string, string> = buildSessionHeaders(
798
793
  this.prefix,
799
- options?.cache,
800
794
  options?.headers,
801
795
  sessionId
802
796
  );
803
- for (const [k, v] of Object.entries(headers)) {
804
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete headers[k];
805
- }
806
797
  // Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
807
798
  // `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
808
799
  // drops affinity headers that trigger CORS preflights / strict 400s.
@@ -891,18 +882,15 @@ export class OpenAICompatibleChatProvider implements Provider {
891
882
  const baseUrl = this.baseUrl(options);
892
883
  const apiKey = this.requireApiKey(baseUrl, options);
893
884
  const url = `${baseUrl}/chat/completions`;
894
- const sessionId = options?.sessionId || options?.cache?.sessionId;
885
+ const sessionId = options?.sessionId;
895
886
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
896
887
  const body = await this.buildBody(clean, context, options, tools, false);
897
888
 
898
889
  const auditRaw: Record<string, string> = {
899
- ...buildSessionHeaders(this.prefix, options?.cache, options?.headers, sessionId),
890
+ ...buildSessionHeaders(this.prefix, options?.headers, sessionId),
900
891
  "Content-Type": "application/json",
901
892
  ...(apiKey ? { Authorization: "[REDACTED]" } : {}),
902
893
  };
903
- for (const k of Object.keys(auditRaw)) {
904
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete auditRaw[k];
905
- }
906
894
  const rawRequest = {
907
895
  url,
908
896
  method: "POST",
@@ -986,18 +974,15 @@ export class OpenAICompatibleChatProvider implements Provider {
986
974
  const baseUrl = this.baseUrl(options);
987
975
  const apiKey = this.requireApiKey(baseUrl, options);
988
976
  const url = `${baseUrl}/chat/completions`;
989
- const sessionId = options?.sessionId || options?.cache?.sessionId;
977
+ const sessionId = options?.sessionId;
990
978
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
991
979
  const body = await this.buildBody(clean, context, options, tools, true);
992
980
 
993
981
  const streamAuditRaw: Record<string, string> = {
994
- ...buildSessionHeaders(prefix, options?.cache, options?.headers, sessionId),
982
+ ...buildSessionHeaders(prefix, options?.headers, sessionId),
995
983
  "Content-Type": "application/json",
996
984
  ...(apiKey ? { Authorization: "[REDACTED]" } : {}),
997
985
  };
998
- for (const k of Object.keys(streamAuditRaw)) {
999
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete streamAuditRaw[k];
1000
- }
1001
986
  const rawRequest = {
1002
987
  url,
1003
988
  method: "POST",
@@ -75,7 +75,7 @@ import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutpu
75
75
  import {
76
76
  mapThinkingLevelToOpenAI,
77
77
  mapServiceTierToOpenAI,
78
- applyCacheForOpenAI,
78
+ resolveCache,
79
79
  mapToolChoiceToOpenAI,
80
80
  noteProviderTurn,
81
81
  parseStreamedToolArguments,
@@ -85,6 +85,7 @@ import {
85
85
  normalizeStructuredOutput,
86
86
  augmentSystemPromptWithStructuredOutput,
87
87
  emitProviderWarning,
88
+ warnIgnoredMediaResolution,
88
89
  } from "../providers.ts";
89
90
 
90
91
  // ---------------------------------------------------------------------------
@@ -156,19 +157,6 @@ interface ResponsesObject {
156
157
 
157
158
  const DEFAULT_BASE_URL = "https://api.openai.com/v1";
158
159
 
159
- /** Q-18: Responses transport never echoes thought signatures (no wire field).
160
- * Local helper documents the namespacing decision per provider file. */
161
- function withProviderSignature(_sig: string | undefined): { thoughtSignature?: undefined } {
162
- return {};
163
- }
164
-
165
- /** Internal headers that must never leak onto native REST requests. */
166
- const INTERNAL_HEADERS = new Set([
167
- "x-thought-signature-map",
168
- "x-cached-content-id",
169
- "x-multimodal-user-content",
170
- ]);
171
-
172
160
  // ---------------------------------------------------------------------------
173
161
  // Request building (canonical -> Responses)
174
162
  // ---------------------------------------------------------------------------
@@ -535,7 +523,9 @@ export class OpenAIResponsesProvider implements Provider {
535
523
  ): Promise<ResponsesRequestBody> {
536
524
  const { effort } = mapThinkingLevelToOpenAI(options?.thinking?.level);
537
525
  const serviceTier = mapServiceTierToOpenAI(options?.serviceTier);
538
- applyCacheForOpenAI(options?.cache, `openai/${modelId}`);
526
+ // `12h` maps to `prompt_cache_retention: "24h"`; everything else stays on implicit prompt_cache_key affinity.
527
+ const cache = resolveCache("openai", options?.cache, ["implicit", "12h"], `openai/${modelId}`);
528
+ warnIgnoredMediaResolution("openai", context.messages, `openai/${modelId}`);
539
529
 
540
530
  // Structured outputs: native `text.format` when supported, else emulated
541
531
  // via instructions (catalog `structured_output: false` would 400).
@@ -598,10 +588,8 @@ export class OpenAIResponsesProvider implements Provider {
598
588
  const cacheKey = clampCacheKey(sessionId);
599
589
  if (cacheKey) body.prompt_cache_key = cacheKey;
600
590
  }
601
- // Long retention: `prompt_cache_retention: "24h"` when the catalog
602
- // supports it (pi api/openai-responses.ts:97 rule). Other explicit modes
603
- // stay warn+drop via applyCacheForOpenAI above.
604
- if (options?.cache?.retention === "long") {
591
+ // Long retention: `prompt_cache_retention: "24h"` when the catalog supports it.
592
+ if (cache === "12h") {
605
593
  let supportsLong = true;
606
594
  try {
607
595
  const spec = getModelFromCatalog("openai", modelId);
@@ -614,8 +602,8 @@ export class OpenAIResponsesProvider implements Provider {
614
602
  } else {
615
603
  emitProviderWarning({
616
604
  provider: "openai",
617
- capability: "cache retention",
618
- requested: `long (openai/${modelId})`,
605
+ capability: "cache",
606
+ requested: `12h (openai/${modelId})`,
619
607
  reason: "the catalog reports no long-cache support for this model.",
620
608
  fallback: "default caching with prompt_cache_key affinity (no retention payload is sent)",
621
609
  });
@@ -635,13 +623,9 @@ export class OpenAIResponsesProvider implements Provider {
635
623
  ): RequestInit {
636
624
  const headers: Record<string, string> = buildSessionHeaders(
637
625
  "openai",
638
- options?.cache,
639
626
  options?.headers,
640
627
  sessionId
641
628
  );
642
- for (const [k, v] of Object.entries(headers)) {
643
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete headers[k];
644
- }
645
629
  // Case-insensitive merge so a lowercase `authorization` custom header
646
630
  // never coexists with the canonical bearer. Proxy flows
647
631
  // (`apiKey === "unused"`) preserve custom auth instead of overwriting.
@@ -723,7 +707,7 @@ export class OpenAIResponsesProvider implements Provider {
723
707
  assertNoVideoPartsOnResponses(context, "openai", clean);
724
708
  const baseUrl = resolveBaseUrl(options);
725
709
  const url = `${baseUrl}/responses`;
726
- const sessionId = options?.sessionId || options?.cache?.sessionId;
710
+ const sessionId = options?.sessionId;
727
711
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
728
712
  const body = await this.buildBody(clean, context, options, sessionId, tools, false);
729
713
 
@@ -731,13 +715,10 @@ export class OpenAIResponsesProvider implements Provider {
731
715
  // redacted. Previously only Content-Type + custom headers were stored,
732
716
  // hiding the x-session-id affinity actually sent.
733
717
  const auditHeadersRaw: Record<string, string> = {
734
- ...buildSessionHeaders("openai", options?.cache, options?.headers, sessionId),
718
+ ...buildSessionHeaders("openai", options?.headers, sessionId),
735
719
  "Content-Type": "application/json",
736
720
  Authorization: "[REDACTED]",
737
721
  };
738
- for (const k of Object.keys(auditHeadersRaw)) {
739
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete auditHeadersRaw[k];
740
- }
741
722
  const auditHeaders = redactedHeaders(auditHeadersRaw);
742
723
  const rawRequest = {
743
724
  url,
@@ -825,18 +806,15 @@ export class OpenAIResponsesProvider implements Provider {
825
806
  assertNoVideoPartsOnResponses(context, "openai", clean);
826
807
  const baseUrl = resolveBaseUrl(options);
827
808
  const url = `${baseUrl}/responses`;
828
- const sessionId = options?.sessionId || options?.cache?.sessionId;
809
+ const sessionId = options?.sessionId;
829
810
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
830
811
  const body = await this.buildBody(clean, context, options, sessionId, tools, true);
831
812
 
832
813
  const streamAuditRaw: Record<string, string> = {
833
- ...buildSessionHeaders("openai", options?.cache, options?.headers, sessionId),
814
+ ...buildSessionHeaders("openai", options?.headers, sessionId),
834
815
  "Content-Type": "application/json",
835
816
  Authorization: "[REDACTED]",
836
817
  };
837
- for (const k of Object.keys(streamAuditRaw)) {
838
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete streamAuditRaw[k];
839
- }
840
818
  const rawRequest = {
841
819
  url,
842
820
  method: "POST",
@@ -67,7 +67,7 @@ import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutpu
67
67
  import {
68
68
  mapThinkingLevelToOpenRouterChat,
69
69
  mapServiceTierToOpenRouter,
70
- applyCacheForOpenRouter,
70
+ resolveCache,
71
71
  mapToolChoiceToOpenRouterChat,
72
72
  noteProviderTurn,
73
73
  parseStreamedToolArguments,
@@ -77,6 +77,7 @@ import {
77
77
  normalizeStructuredOutput,
78
78
  augmentSystemPromptWithStructuredOutput,
79
79
  emitProviderWarning,
80
+ warnIgnoredMediaResolution,
80
81
  } from "../providers.ts";
81
82
 
82
83
  // ---------------------------------------------------------------------------
@@ -115,6 +116,7 @@ interface ChatRequestBody {
115
116
  service_tier?: string;
116
117
  session_id?: string;
117
118
  plugins?: Array<{ id: string }>;
119
+ cache_control?: { type: "ephemeral"; ttl: "5m" | "1h" };
118
120
  response_format?: { type: "json_schema"; json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
119
121
  stream?: boolean;
120
122
  [k: string]: unknown;
@@ -180,19 +182,6 @@ interface ChatResponse {
180
182
 
181
183
  const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
182
184
 
183
- /** Q-18: Chat transport never echoes thought signatures (no wire field).
184
- * Local helper documents the namespacing decision per provider file. */
185
- function withProviderSignature(_sig: string | undefined): { thoughtSignature?: undefined } {
186
- return {};
187
- }
188
-
189
- /** Internal headers that must never leak onto native REST requests. */
190
- const INTERNAL_HEADERS = new Set([
191
- "x-thought-signature-map",
192
- "x-cached-content-id",
193
- "x-multimodal-user-content",
194
- ]);
195
-
196
185
  // ---------------------------------------------------------------------------
197
186
  // Request building (canonical -> Chat Completions)
198
187
  // ---------------------------------------------------------------------------
@@ -586,7 +575,9 @@ export class OpenRouterChatCompletionsProvider implements Provider {
586
575
  ): Promise<ChatRequestBody> {
587
576
  const { effort } = mapThinkingLevelToOpenRouterChat(options?.thinking?.level);
588
577
  const serviceTier = mapServiceTierToOpenRouter(options?.serviceTier);
589
- applyCacheForOpenRouter(options?.cache, `openrouter/${modelId}`);
578
+ // Anthropic models take a top-level `cache_control` TTL; every other route caches implicitly (docs: prompt-caching.md).
579
+ const cache = resolveCache("openrouter", options?.cache, modelId.startsWith("anthropic/") ? ["implicit", "5m", "1h"] : ["implicit"], `openrouter/${modelId}`);
580
+ warnIgnoredMediaResolution("openrouter", context.messages, `openrouter/${modelId}`);
590
581
 
591
582
  // Structured outputs: native `response_format` when supported, else
592
583
  // emulated via system-prompt instruction + client validation (catalog
@@ -628,6 +619,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
628
619
  if (toolChoice !== undefined) body.tool_choice = toolChoice;
629
620
  if (effort) body.reasoning = { effort };
630
621
  if (serviceTier) body.service_tier = serviceTier;
622
+ if (cache === "5m" || cache === "1h") body.cache_control = { type: "ephemeral", ttl: cache };
631
623
  // Documented Chat Completions generation controls (only those the caller set).
632
624
  Object.assign(body, mapGenerationParamsToChat(options));
633
625
  // Structured outputs: Chat Completions `response_format{type:json_schema,json_schema}`.
@@ -665,13 +657,9 @@ export class OpenRouterChatCompletionsProvider implements Provider {
665
657
  // (HTTP-Referer / X-Title / x-session-id / x-client-request-id).
666
658
  const headers: Record<string, string> = buildSessionHeaders(
667
659
  "openrouter",
668
- options?.cache,
669
660
  options?.headers,
670
661
  sessionId
671
662
  );
672
- for (const [k, v] of Object.entries(headers)) {
673
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete headers[k];
674
- }
675
663
  setHeaderCaseInsensitive(headers, "Content-Type", "application/json");
676
664
  if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "Authorization", `Bearer ${apiKey}`);
677
665
  return { method: "POST", headers, body: JSON.stringify(body), signal };
@@ -747,20 +735,17 @@ export class OpenRouterChatCompletionsProvider implements Provider {
747
735
  assertModalitiesSupported(context, "openrouter", clean);
748
736
  const baseUrl = resolveBaseUrl(options);
749
737
  const url = `${baseUrl}/chat/completions`;
750
- const sessionId = options?.sessionId || options?.cache?.sessionId;
738
+ const sessionId = options?.sessionId;
751
739
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
752
740
  const body = await this.buildBody(clean, context, options, sessionId, tools, false);
753
741
 
754
742
  // Audit trail: record the actual wire headers (attribution + session
755
743
  // affinity included), redacted.
756
744
  const auditRaw: Record<string, string> = {
757
- ...buildSessionHeaders("openrouter", options?.cache, options?.headers, sessionId),
745
+ ...buildSessionHeaders("openrouter", options?.headers, sessionId),
758
746
  "Content-Type": "application/json",
759
747
  Authorization: "[REDACTED]",
760
748
  };
761
- for (const k of Object.keys(auditRaw)) {
762
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete auditRaw[k];
763
- }
764
749
  const rawRequest = {
765
750
  url,
766
751
  method: "POST",
@@ -846,18 +831,15 @@ export class OpenRouterChatCompletionsProvider implements Provider {
846
831
  assertModalitiesSupported(context, "openrouter", clean);
847
832
  const baseUrl = resolveBaseUrl(options);
848
833
  const url = `${baseUrl}/chat/completions`;
849
- const sessionId = options?.sessionId || options?.cache?.sessionId;
834
+ const sessionId = options?.sessionId;
850
835
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
851
836
  const body = await this.buildBody(clean, context, options, sessionId, tools, true);
852
837
 
853
838
  const streamAuditRaw: Record<string, string> = {
854
- ...buildSessionHeaders("openrouter", options?.cache, options?.headers, sessionId),
839
+ ...buildSessionHeaders("openrouter", options?.headers, sessionId),
855
840
  "Content-Type": "application/json",
856
841
  Authorization: "[REDACTED]",
857
842
  };
858
- for (const k of Object.keys(streamAuditRaw)) {
859
- if (INTERNAL_HEADERS.has(k.toLowerCase())) delete streamAuditRaw[k];
860
- }
861
843
  const rawRequest = {
862
844
  url,
863
845
  method: "POST",
package/src/providers.ts CHANGED
@@ -9,8 +9,9 @@
9
9
  * Rule: OpenAI, OpenRouter, and future providers must be addable here without
10
10
  * redesigning this abstraction — only new per-provider mappers/adapters.
11
11
  */
12
- import type { CacheConfig, ServiceTier, ThinkingLevel } from "./types/core.ts";
12
+ import type { CacheOption, ServiceTier, ThinkingLevel } from "./types/core.ts";
13
13
  import type { StructuredOutputSpec } from "./types/model.ts";
14
+ import type { Message } from "./types/message.ts";
14
15
  import { zodToJsonSchema } from "./tools/schema.ts";
15
16
  import { ValidationError } from "./types/errors.ts";
16
17
  import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
@@ -30,14 +31,14 @@ export type ProviderCapabilityStatus =
30
31
  * There is no pre-existing logger in `src/` (only CLI logs in
31
32
  * `update-models.ts`), so this `console.warn` line IS the mechanism — do not
32
33
  * introduce a second one. Used when a requested capability cannot be applied
33
- * as-is (e.g. Google explicit cache retention) so it never silently
34
+ * as-is (e.g. an unsupported `cache` TTL) so it never silently
34
35
  * disappears.
35
36
  *
36
37
  * Warnings are deduped per provider+capability+requested: agent loops call
37
38
  * mappers once per turn, and repeating the same warning every turn is noise.
38
39
  * A repeated identical request logs once per process.
39
40
  *
40
- * @example `emitProviderWarning({ provider: "google", capability: "cache retention", requested: "high", reason: "...", fallback: "..." })`
41
+ * @example `emitProviderWarning({ provider: "google", capability: "cache", requested: "12h", reason: "...", fallback: "..." })`
41
42
  */
42
43
  const emittedWarnings = new Set<string>();
43
44
 
@@ -229,68 +230,37 @@ export function mapServiceTierToOpenRouter(tier: ServiceTier | undefined): "flex
229
230
  return undefined;
230
231
  }
231
232
 
232
- /**
233
- * Applies canonical cache config for OpenRouter. Session affinity flows via
234
- * top-level body `session_id` (sent by the adapter) plus the `x-session-id`
235
- * header fallback; there is no retention body primitive, so explicit
236
- * retention/cachedContentId are UNSUPPORTED: warn and drop.
237
- */
238
- export function applyCacheForOpenRouter(
239
- cache: CacheConfig | undefined,
233
+ /** Returns the `cache` value a provider will honour; an unsupported one warns once and falls back to the provider default (`undefined`). In: `ProviderRequestOptions.cache` from each adapter's buildBody, out: the adapter's wire mapping. */
234
+ export function resolveCache(
235
+ provider: string,
236
+ cache: CacheOption | undefined,
237
+ supported: readonly CacheOption[],
240
238
  modelRef: string
241
- ): void {
242
- if (!cache) return;
243
- if (cache.retention && cache.retention !== "implicit") {
244
- emitProviderWarning({
245
- provider: "openrouter",
246
- capability: "cache retention",
247
- requested: `${cache.retention} (${modelRef})`,
248
- reason: "OpenRouter documents no retention control on the stable Chat Completions endpoint.",
249
- fallback: "default gateway caching with headers-only session affinity (no retention payload is sent)",
250
- });
251
- }
252
- if (cache.cachedContentId) {
253
- emitProviderWarning({
254
- provider: "openrouter",
255
- capability: "explicit cached content",
256
- requested: cache.cachedContentId,
257
- reason: "cached content references do not exist on the Responses API.",
258
- fallback: "full-history sends instead (cachedContentId is ignored)",
259
- });
260
- }
239
+ ): CacheOption | undefined {
240
+ if (cache === undefined || supported.includes(cache)) return cache;
241
+ emitProviderWarning({
242
+ provider,
243
+ capability: "cache",
244
+ requested: `${cache} (${modelRef})`,
245
+ reason: `supported values here are: ${supported.map((v) => `"${v}"`).join(", ")}.`,
246
+ fallback: "the provider default (implicit caching)",
247
+ });
248
+ return undefined;
261
249
  }
262
250
 
263
- /**
264
- * Applies canonical cache config for custom OpenAI-compatible endpoints.
265
- * Affinity is headers-only (`x-session-id`): unknown body properties make
266
- * strict endpoints (groq, ollama, …) fail, so nothing cache-related is ever
267
- * placed in the body. Explicit retention/cachedContentId are UNSUPPORTED:
268
- * warn and drop.
269
- */
270
- export function applyCacheForCustom(
271
- prefix: string,
272
- cache: CacheConfig | undefined,
273
- modelRef: string
274
- ): void {
275
- if (!cache) return;
276
- if (cache.retention && cache.retention !== "implicit") {
277
- emitProviderWarning({
278
- provider: prefix,
279
- capability: "cache retention",
280
- requested: `${cache.retention} (${modelRef})`,
281
- reason: "custom OpenAI-compatible endpoints define no retention control, and strict endpoints reject unknown body properties.",
282
- fallback: "default caching with headers-only session affinity (no retention payload is sent)",
283
- });
284
- }
285
- if (cache.cachedContentId) {
286
- emitProviderWarning({
287
- provider: prefix,
288
- capability: "explicit cached content",
289
- requested: cache.cachedContentId,
290
- reason: "cached content references do not exist on OpenAI-compatible endpoints.",
291
- fallback: "full-history sends instead (cachedContentId is ignored)",
292
- });
293
- }
251
+ /** Warns once per provider/model that per-media `resolution` is ignored (no such control on that wire); in: an adapter's request messages, out: telemetry bus only. */
252
+ export function warnIgnoredMediaResolution(provider: string, messages: Message[], modelRef: string): void {
253
+ const used = messages.some(
254
+ (m) => Array.isArray(m.content) && m.content.some((p) => (p.type === "image" || p.type === "video") && p.resolution)
255
+ );
256
+ if (!used) return;
257
+ emitProviderWarning({
258
+ provider,
259
+ capability: "media resolution",
260
+ requested: modelRef,
261
+ reason: "this provider has no per-media detail control (Google only).",
262
+ fallback: "the provider's default detail",
263
+ });
294
264
  }
295
265
 
296
266
  /** OpenRouter Chat Completions `tool_choice` wire values. */
@@ -368,38 +338,6 @@ export function mapServiceTierToOpenAI(tier: ServiceTier | undefined): "flex" |
368
338
  return undefined;
369
339
  }
370
340
 
371
- /**
372
- * Applies canonical cache config for OpenAI. Session affinity flows via
373
- * `prompt_cache_key` (handled by the adapter); `long` retention maps to
374
- * `prompt_cache_retention: "24h"` when the catalog supports it (handled by
375
- * the adapter, pi `api/openai-responses.ts:97` rule). Other explicit
376
- * retention modes and `cachedContentId` are UNSUPPORTED: warn and drop.
377
- */
378
- export function applyCacheForOpenAI(
379
- cache: CacheConfig | undefined,
380
- modelRef: string
381
- ): void {
382
- if (!cache) return;
383
- if (cache.retention && cache.retention !== "implicit" && cache.retention !== "long") {
384
- emitProviderWarning({
385
- provider: "openai",
386
- capability: "cache retention",
387
- requested: `${cache.retention} (${modelRef})`,
388
- reason: "the native Responses adapter uses prompt_cache_key affinity only; explicit retention modes are out of scope.",
389
- fallback: "default caching with prompt_cache_key affinity (no retention payload is sent)",
390
- });
391
- }
392
- if (cache.cachedContentId) {
393
- emitProviderWarning({
394
- provider: "openai",
395
- capability: "explicit cached content",
396
- requested: cache.cachedContentId,
397
- reason: "cached content references do not exist on the Responses API.",
398
- fallback: "full-history sends instead (cachedContentId is ignored)",
399
- });
400
- }
401
- }
402
-
403
341
  /** OpenAI Responses `tool_choice` wire values (most-important subset). */
404
342
  export type OpenAIToolChoice = "auto" | "none" | "required" | { type: "function"; name: string };
405
343
 
@@ -419,36 +357,6 @@ export function mapToolChoiceToOpenAI(
419
357
  return undefined;
420
358
  }
421
359
 
422
- /**
423
- * Applies canonical cache config for Google. The Interactions API supports
424
- * implicit/automatic caching ONLY — there is no cache payload to send.
425
- * Explicit retention/cachedContentId are UNSUPPORTED: warn and drop.
426
- */
427
- export function applyCacheForGoogle(
428
- cache: CacheConfig | undefined,
429
- modelRef: string
430
- ): void {
431
- if (!cache) return;
432
- if (cache.retention && cache.retention !== "implicit") {
433
- emitProviderWarning({
434
- provider: "google",
435
- capability: "cache retention",
436
- requested: `${cache.retention} (${modelRef})`,
437
- reason: "the Interactions API does not support user-defined explicit cache retention.",
438
- fallback: "Google automatic/implicit caching (no cache payload is sent)",
439
- });
440
- }
441
- if (cache.cachedContentId) {
442
- emitProviderWarning({
443
- provider: "google",
444
- capability: "explicit cached content",
445
- requested: cache.cachedContentId,
446
- reason: "cachedContents resources do not exist on the Interactions API.",
447
- fallback: "stateful interaction chaining / stateless history instead (cachedContentId is ignored)",
448
- });
449
- }
450
- }
451
-
452
360
  /** Normalized tool-choice mode shared by provider adapters. */
453
361
  export type CanonicalToolChoiceMode = "auto" | "any" | "none";
454
362