@fgv/ts-extras 5.1.0-33 → 5.1.0-35

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 (108) hide show
  1. package/dist/packlets/ai-assist/apiClient.js +58 -112
  2. package/dist/packlets/ai-assist/apiClient.js.map +1 -1
  3. package/dist/packlets/ai-assist/chatRequestBuilders.js +131 -35
  4. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  5. package/dist/packlets/ai-assist/converters.js +31 -1
  6. package/dist/packlets/ai-assist/converters.js.map +1 -1
  7. package/dist/packlets/ai-assist/embeddingClient.js +346 -0
  8. package/dist/packlets/ai-assist/embeddingClient.js.map +1 -0
  9. package/dist/packlets/ai-assist/http.js +75 -0
  10. package/dist/packlets/ai-assist/http.js.map +1 -0
  11. package/dist/packlets/ai-assist/index.js +6 -4
  12. package/dist/packlets/ai-assist/index.js.map +1 -1
  13. package/dist/packlets/ai-assist/jsonCompletion.js +6 -8
  14. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -1
  15. package/dist/packlets/ai-assist/model.js +36 -1
  16. package/dist/packlets/ai-assist/model.js.map +1 -1
  17. package/dist/packlets/ai-assist/registry.js +77 -7
  18. package/dist/packlets/ai-assist/registry.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +176 -32
  20. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  21. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +528 -0
  22. package/dist/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
  23. package/dist/packlets/ai-assist/streamingAdapters/common.js +95 -0
  24. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  25. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +34 -10
  26. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  27. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +215 -15
  28. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  29. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +15 -8
  30. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  31. package/dist/packlets/ai-assist/streamingClient.js +29 -5
  32. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  33. package/dist/packlets/ai-assist/thinkingOptionsResolver.js +23 -0
  34. package/dist/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  35. package/dist/packlets/ai-assist/toolFormats.js +106 -10
  36. package/dist/packlets/ai-assist/toolFormats.js.map +1 -1
  37. package/dist/ts-extras.d.ts +682 -48
  38. package/lib/packlets/ai-assist/apiClient.d.ts +24 -34
  39. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  40. package/lib/packlets/ai-assist/apiClient.js +67 -121
  41. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  42. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +82 -22
  43. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -1
  44. package/lib/packlets/ai-assist/chatRequestBuilders.js +132 -34
  45. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -1
  46. package/lib/packlets/ai-assist/converters.d.ts +9 -1
  47. package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
  48. package/lib/packlets/ai-assist/converters.js +31 -1
  49. package/lib/packlets/ai-assist/converters.js.map +1 -1
  50. package/lib/packlets/ai-assist/embeddingClient.d.ts +69 -0
  51. package/lib/packlets/ai-assist/embeddingClient.d.ts.map +1 -0
  52. package/lib/packlets/ai-assist/embeddingClient.js +350 -0
  53. package/lib/packlets/ai-assist/embeddingClient.js.map +1 -0
  54. package/lib/packlets/ai-assist/http.d.ts +24 -0
  55. package/lib/packlets/ai-assist/http.d.ts.map +1 -0
  56. package/lib/packlets/ai-assist/http.js +78 -0
  57. package/lib/packlets/ai-assist/http.js.map +1 -0
  58. package/lib/packlets/ai-assist/index.d.ts +6 -4
  59. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  60. package/lib/packlets/ai-assist/index.js +11 -1
  61. package/lib/packlets/ai-assist/index.js.map +1 -1
  62. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/jsonCompletion.js +6 -8
  64. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -1
  65. package/lib/packlets/ai-assist/model.d.ts +377 -5
  66. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/model.js +37 -2
  68. package/lib/packlets/ai-assist/model.js.map +1 -1
  69. package/lib/packlets/ai-assist/registry.d.ts +23 -1
  70. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/registry.js +79 -7
  72. package/lib/packlets/ai-assist/registry.js.map +1 -1
  73. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +58 -5
  74. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -1
  75. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +175 -31
  76. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -1
  77. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts +172 -0
  78. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.d.ts.map +1 -0
  79. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js +534 -0
  80. package/lib/packlets/ai-assist/streamingAdapters/clientToolContinuationBuilder.js.map +1 -0
  81. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +59 -11
  82. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  83. package/lib/packlets/ai-assist/streamingAdapters/common.js +97 -0
  84. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  85. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +16 -2
  86. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -1
  87. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +34 -10
  88. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -1
  89. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +15 -2
  90. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  91. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +214 -14
  92. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  93. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -1
  94. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +14 -7
  95. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -1
  96. package/lib/packlets/ai-assist/streamingClient.d.ts +17 -0
  97. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  98. package/lib/packlets/ai-assist/streamingClient.js +31 -6
  99. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  100. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts +18 -2
  101. package/lib/packlets/ai-assist/thinkingOptionsResolver.d.ts.map +1 -1
  102. package/lib/packlets/ai-assist/thinkingOptionsResolver.js +24 -0
  103. package/lib/packlets/ai-assist/thinkingOptionsResolver.js.map +1 -1
  104. package/lib/packlets/ai-assist/toolFormats.d.ts +40 -9
  105. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -1
  106. package/lib/packlets/ai-assist/toolFormats.js +107 -10
  107. package/lib/packlets/ai-assist/toolFormats.js.map +1 -1
  108. package/package.json +7 -7
@@ -19,74 +19,26 @@
19
19
  // SOFTWARE.
20
20
  /**
21
21
  * Chat completion client for AI assist with support for multiple provider APIs.
22
- *
23
22
  * Supports OpenAI-compatible providers (xAI, OpenAI, Groq, Mistral) directly,
24
- * plus adapters for Anthropic and Google Gemini.
25
- *
26
- * When server-side tools (e.g. web_search) are configured, providers that support
27
- * them will include tool configuration in the request and handle tool-augmented
28
- * responses.
23
+ * plus adapters for Anthropic and Google Gemini. When server-side tools (e.g.
24
+ * web_search) are configured, providers that support them include tool
25
+ * configuration in the request and handle tool-augmented responses.
29
26
  *
30
27
  * @packageDocumentation
31
28
  */
32
29
  import { isJsonObject } from '@fgv/ts-json-base';
33
30
  import { fail, mapResults, succeed, Validators } from '@fgv/ts-utils';
34
- import { resolveModel } from './model';
35
- import { checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
36
- import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent } from './chatRequestBuilders';
31
+ import { allModelCapabilities, resolveModel } from './model';
32
+ import { anthropicEffortToBudgetTokens, checkTemperatureConflict, mergeThinkingConfig, providerDiscriminatorForId } from './thinkingOptionsResolver';
33
+ import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent, normalizeOutboundMessages, splitChatRequest } from './chatRequestBuilders';
37
34
  import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
35
+ import { fetchJson } from './http';
38
36
  import { DEFAULT_MODEL_CAPABILITY_CONFIG, resolveImageCapability, supportsImageGeneration } from './registry';
39
37
  import { resolveImageOptions, validateResolvedOptions } from './imageOptionsResolver';
40
38
  import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
41
39
  // ============================================================================
42
40
  // Shared helpers
43
41
  // ============================================================================
44
- /**
45
- * Makes an HTTP request and returns the parsed JSON, or a failure.
46
- * @internal
47
- */
48
- async function fetchJson(url, headers, body, logger, signal) {
49
- /* c8 ignore next 1 - optional logger */
50
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url}`);
51
- let response;
52
- try {
53
- response = await fetch(url, {
54
- method: 'POST',
55
- headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
56
- body: JSON.stringify(body),
57
- signal
58
- });
59
- }
60
- catch (err) {
61
- const detail = err instanceof Error ? err.message : String(err);
62
- /* c8 ignore next 1 - optional logger */
63
- logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
64
- return fail(`AI API request failed: ${detail}`);
65
- }
66
- if (!response.ok) {
67
- const errorText = await response.text().catch(() => 'unknown error');
68
- /* c8 ignore next 1 - optional logger */
69
- logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
70
- return fail(`AI API returned ${response.status}: ${errorText}`);
71
- }
72
- /* c8 ignore next 1 - optional logger */
73
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
74
- let json;
75
- try {
76
- json = await response.json();
77
- }
78
- catch (_a) {
79
- /* c8 ignore next 1 - optional logger */
80
- logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
81
- return fail('AI API returned invalid JSON response');
82
- }
83
- if (!isJsonObject(json)) {
84
- /* c8 ignore next 1 - optional logger */
85
- logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
86
- return fail('AI API returned non-object JSON response');
87
- }
88
- return succeed(json);
89
- }
90
42
  /**
91
43
  * Makes a multipart/form-data POST request and returns the parsed JSON, or a
92
44
  * failure. The Content-Type header (with boundary) is set automatically by
@@ -269,11 +221,11 @@ const geminiResponse = Validators.object({
269
221
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
270
222
  * @internal
271
223
  */
272
- async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, signal, resolvedThinking) {
224
+ async function callOpenAiCompletion(config, prompt, head, temperature = 0.7, logger, signal, resolvedThinking) {
273
225
  var _a;
274
226
  const url = `${config.baseUrl}/chat/completions`;
275
227
  const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
276
- tail: additionalMessages
228
+ head
277
229
  });
278
230
  const effort = (_a = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.openAiEffort) !== null && _a !== void 0 ? _a : resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.xaiEffort;
279
231
  const body = Object.assign(Object.assign({ model: config.model, messages }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning_effort: effort } : {}));
@@ -322,11 +274,11 @@ function extractResponsesApiText(output) {
322
274
  * Used when tools are configured for an openai-format provider.
323
275
  * @internal
324
276
  */
325
- async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger, signal, resolvedThinking) {
277
+ async function callOpenAiResponsesCompletion(config, prompt, tools, head, temperature = 0.7, logger, signal, resolvedThinking) {
326
278
  var _a;
327
279
  const url = `${config.baseUrl}/responses`;
328
280
  const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
329
- tail: additionalMessages
281
+ head
330
282
  });
331
283
  const effort = (_a = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.openAiEffort) !== null && _a !== void 0 ? _a : resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.xaiEffort;
332
284
  const body = Object.assign(Object.assign({ model: config.model, input, tools: toResponsesApiTools(tools) }, (effort === undefined || effort === 'none' ? { temperature } : {})), (effort !== undefined && config.model !== 'grok-4' ? { reasoning: { effort } } : {}));
@@ -376,13 +328,13 @@ function extractAnthropicText(content) {
376
328
  return succeed(textParts.join(''));
377
329
  }
378
330
  /** Calls the Anthropic Messages API with optional tool support. @internal */
379
- async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal, resolvedThinking) {
331
+ async function callAnthropicCompletion(config, prompt, head, temperature = 0.7, logger, tools, signal, resolvedThinking) {
380
332
  const url = `${config.baseUrl}/messages`;
381
- const messages = buildAnthropicMessages(prompt, { tail: additionalMessages });
333
+ const messages = buildAnthropicMessages(prompt, { head });
382
334
  const body = Object.assign({ model: config.model, system: prompt.system, messages, max_tokens: 4096 }, ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort) === undefined ? { temperature } : {}));
383
- if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort) !== undefined) {
384
- body.thinking = { type: 'enabled' };
385
- body.output_config = { effort: resolvedThinking.anthropicEffort };
335
+ const effort = resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.anthropicEffort;
336
+ if (effort !== undefined) {
337
+ body.thinking = { type: 'enabled', budget_tokens: anthropicEffortToBudgetTokens(effort) };
386
338
  }
387
339
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.otherParams) !== undefined) {
388
340
  Object.assign(body, resolvedThinking.otherParams);
@@ -426,9 +378,9 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
426
378
  * When tools are configured, includes Google Search grounding.
427
379
  * @internal
428
380
  */
429
- async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal, resolvedThinking) {
381
+ async function callGeminiCompletion(config, prompt, head, temperature = 0.7, logger, tools, signal, resolvedThinking) {
430
382
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
431
- const contents = buildGeminiContents(prompt, { tail: additionalMessages });
383
+ const contents = buildGeminiContents(prompt, { head });
432
384
  const generationConfig = { temperature };
433
385
  if ((resolvedThinking === null || resolvedThinking === void 0 ? void 0 : resolvedThinking.geminiThinkingBudget) !== undefined) {
434
386
  generationConfig.thinkingConfig = { thinkingBudget: resolvedThinking.geminiThinkingBudget };
@@ -472,16 +424,19 @@ async function callGeminiCompletion(config, prompt, additionalMessages, temperat
472
424
  // Provider dispatcher
473
425
  // ============================================================================
474
426
  /**
475
- * Calls the appropriate chat completion API for a given provider.
476
- * Routes by `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API when
477
- * tools are set), `'anthropic'`, or `'gemini'`.
478
- * @param params - Request parameters including descriptor, API key, prompt, and optional tools
479
- * @returns The completion response with content and truncation status, or a failure
427
+ * Calls the appropriate chat completion API for a given provider. Routes by
428
+ * `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API
429
+ * when tools are set), `'anthropic'`, or `'gemini'`.
480
430
  * @public
481
431
  */
482
432
  export async function callProviderCompletion(params) {
483
433
  var _a;
484
- const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal, endpoint, thinking } = params;
434
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, endpoint, thinking } = params;
435
+ const splitResult = splitChatRequest(system, messages);
436
+ if (splitResult.isFailure()) {
437
+ return fail(splitResult.message);
438
+ }
439
+ const { prompt, head } = splitResult.value;
485
440
  const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
486
441
  if (baseUrlResult.isFailure()) {
487
442
  return fail(baseUrlResult.message);
@@ -530,13 +485,13 @@ export async function callProviderCompletion(params) {
530
485
  switch (descriptor.apiFormat) {
531
486
  case 'openai':
532
487
  if (hasTools) {
533
- return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, effectiveTemperature, logger, signal, resolvedThinking);
488
+ return callOpenAiResponsesCompletion(config, prompt, tools, head, effectiveTemperature, logger, signal, resolvedThinking);
534
489
  }
535
- return callOpenAiCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, signal, resolvedThinking);
490
+ return callOpenAiCompletion(config, prompt, head, effectiveTemperature, logger, signal, resolvedThinking);
536
491
  case 'anthropic':
537
- return callAnthropicCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, tools, signal, resolvedThinking);
492
+ return callAnthropicCompletion(config, prompt, head, effectiveTemperature, logger, tools, signal, resolvedThinking);
538
493
  case 'gemini':
539
- return callGeminiCompletion(config, prompt, additionalMessages, effectiveTemperature, logger, tools, signal, resolvedThinking);
494
+ return callGeminiCompletion(config, prompt, head, effectiveTemperature, logger, tools, signal, resolvedThinking);
540
495
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
541
496
  default: {
542
497
  const _exhaustive = descriptor.apiFormat;
@@ -587,13 +542,7 @@ const proxiedImageGenerationResponse = Validators.object({
587
542
  });
588
543
  const proxiedListModelsEntry = Validators.object({
589
544
  id: Validators.string,
590
- capabilities: Validators.arrayOf(Validators.enumeratedValue([
591
- 'chat',
592
- 'tools',
593
- 'vision',
594
- 'image-generation',
595
- 'thinking'
596
- ])),
545
+ capabilities: Validators.arrayOf(Validators.enumeratedValue(allModelCapabilities)),
597
546
  displayName: Validators.string.optional()
598
547
  });
599
548
  const proxiedListModelsResponse = Validators.object({
@@ -866,13 +815,12 @@ async function callImagenGeneration(config, request, resolved, logger, signal) {
866
815
  // Image generation — dispatcher
867
816
  // ============================================================================
868
817
  /**
869
- * Calls the appropriate image-generation API for a given provider.
870
- * Routes by the `format` field of the resolved {@link IAiImageModelCapability}:
871
- * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, `'gemini-imagen'`,
872
- * or `'gemini-image-out'`. Rejects up front if `referenceImages` is set but the
818
+ * Calls the appropriate image-generation API for a given provider. Routes by the
819
+ * `format` field of the resolved {@link IAiImageModelCapability}:
820
+ * `'openai-images'`, `'xai-images'`, `'xai-images-edits'`, `'gemini-imagen'`, or
821
+ * `'gemini-image-out'`. Rejects up front if `referenceImages` is set but the
873
822
  * capability does not declare `acceptsImageReferenceInput`.
874
823
  * @param params - Request parameters including descriptor, API key, and prompt
875
- * @returns The generated images, or a failure
876
824
  * @public
877
825
  */
878
826
  export async function callProviderImageGeneration(params) {
@@ -1129,8 +1077,7 @@ async function callGeminiListModels(config, providerId, capabilityConfig, logger
1129
1077
  /**
1130
1078
  * Lists models available from a provider, routing by `descriptor.apiFormat`.
1131
1079
  * Capabilities are resolved from native provider info and a configurable rule set.
1132
- * @param params - Request parameters including descriptor, API key, and optional capability filter
1133
- * @returns The resolved model list, or a failure
1080
+ * @param params - Request parameters (descriptor, API key, optional capability filter)
1134
1081
  * @public
1135
1082
  */
1136
1083
  export async function callProviderListModels(params) {
@@ -1174,10 +1121,10 @@ export async function callProviderListModels(params) {
1174
1121
  // Proxied list models
1175
1122
  // ============================================================================
1176
1123
  /**
1177
- * Calls the model-listing endpoint on a proxy server.
1178
- * Endpoint: `POST ${proxyUrl}/api/ai/list-models`. Capability config is not
1179
- * forwarded. `capabilities` is serialized as a string array. Error body
1180
- * `{error: string}` is surfaced as `proxy: ${error}`.
1124
+ * Calls the model-listing endpoint on a proxy server. Endpoint:
1125
+ * `POST ${proxyUrl}/api/ai/list-models`. Capability config is not forwarded;
1126
+ * `capabilities` is serialized as a string array. Error body `{error: string}`
1127
+ * is surfaced as `proxy: ${error}`.
1181
1128
  * @public
1182
1129
  */
1183
1130
  export async function callProxiedListModels(proxyUrl, params) {
@@ -1212,32 +1159,32 @@ export async function callProxiedListModels(proxyUrl, params) {
1212
1159
  // Proxied completion (routes through a backend server)
1213
1160
  // ============================================================================
1214
1161
  /**
1215
- * Calls the AI completion endpoint on a proxy server instead of calling
1216
- * the provider API directly from the browser.
1217
- *
1218
- * The proxy server handles provider dispatch, CORS, and API key forwarding.
1219
- * The request shape mirrors {@link IProviderCompletionParams} but is serialized
1220
- * as JSON for the proxy endpoint.
1221
- *
1222
- * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
1162
+ * Calls the AI completion endpoint on a proxy server instead of calling the
1163
+ * provider API directly from the browser. The proxy handles provider dispatch,
1164
+ * CORS, and API key forwarding. The request body serializes the unified
1165
+ * {@link AiAssist.IChatRequest} shape (`system?` + `messages`). Enforces the same
1166
+ * non-empty / trailing-user-turn and image-input invariants as the direct path.
1167
+ * @param proxyUrl - Base URL of the proxy server
1223
1168
  * @param params - Same parameters as {@link callProviderCompletion}
1224
- * @returns The completion response, or a failure
1225
1169
  * @public
1226
1170
  */
1227
1171
  export async function callProxiedCompletion(proxyUrl, params) {
1228
- const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal, thinking } = params;
1229
- const promptBody = { system: prompt.system, user: prompt.user };
1230
- if (prompt.attachments.length > 0) {
1231
- promptBody.attachments = prompt.attachments;
1172
+ const { descriptor, apiKey, system, messages, temperature, modelOverride, logger, tools, signal, thinking } = params;
1173
+ const splitResult = splitChatRequest(system, messages);
1174
+ if (splitResult.isFailure()) {
1175
+ return fail(splitResult.message);
1176
+ }
1177
+ if (splitResult.value.prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
1178
+ return fail(`provider "${descriptor.id}" does not accept image input`);
1232
1179
  }
1233
1180
  const body = {
1234
1181
  providerId: descriptor.id,
1235
1182
  apiKey,
1236
- prompt: promptBody,
1183
+ messages: normalizeOutboundMessages(splitResult.value),
1237
1184
  temperature: temperature !== null && temperature !== void 0 ? temperature : 0.7
1238
1185
  };
1239
- if (additionalMessages && additionalMessages.length > 0) {
1240
- body.additionalMessages = additionalMessages;
1186
+ if (system !== undefined) {
1187
+ body.system = system;
1241
1188
  }
1242
1189
  if (modelOverride !== undefined) {
1243
1190
  body.modelOverride = modelOverride;
@@ -1278,9 +1225,8 @@ export async function callProxiedCompletion(proxyUrl, params) {
1278
1225
  * lookup, model resolution, provider dispatch, and response normalization
1279
1226
  * (including repackaging `referenceImages` for the upstream wire format).
1280
1227
  * Error body `{error: string}` is surfaced as `proxy: ${error}`.
1281
- * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
1228
+ * @param proxyUrl - Base URL of the proxy server
1282
1229
  * @param params - Same parameters as {@link callProviderImageGeneration}
1283
- * @returns The generated images, or a failure
1284
1230
  * @public
1285
1231
  */
1286
1232
  export async function callProxiedImageGeneration(proxyUrl, params) {