@fgv/ts-extras 5.1.0-20 → 5.1.0-22

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 (89) hide show
  1. package/dist/packlets/ai-assist/apiClient.js +30 -25
  2. package/dist/packlets/ai-assist/apiClient.js.map +1 -1
  3. package/dist/packlets/ai-assist/converters.js +2 -1
  4. package/dist/packlets/ai-assist/converters.js.map +1 -1
  5. package/dist/packlets/ai-assist/endpoint.js +78 -0
  6. package/dist/packlets/ai-assist/endpoint.js.map +1 -0
  7. package/dist/packlets/ai-assist/index.js +2 -0
  8. package/dist/packlets/ai-assist/index.js.map +1 -1
  9. package/dist/packlets/ai-assist/jsonCompletion.js +95 -0
  10. package/dist/packlets/ai-assist/jsonCompletion.js.map +1 -0
  11. package/dist/packlets/ai-assist/jsonResponse.js +149 -0
  12. package/dist/packlets/ai-assist/jsonResponse.js.map +1 -0
  13. package/dist/packlets/ai-assist/model.js.map +1 -1
  14. package/dist/packlets/ai-assist/registry.js +26 -0
  15. package/dist/packlets/ai-assist/registry.js.map +1 -1
  16. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  17. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +2 -1
  18. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  19. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +2 -1
  20. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  21. package/dist/packlets/ai-assist/streamingClient.js +11 -5
  22. package/dist/packlets/ai-assist/streamingClient.js.map +1 -1
  23. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +6 -0
  24. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -1
  25. package/dist/packlets/crypto-utils/keystore/keyStore.js +81 -0
  26. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  27. package/dist/packlets/crypto-utils/model.js +2 -1
  28. package/dist/packlets/crypto-utils/model.js.map +1 -1
  29. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +21 -2
  30. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
  31. package/dist/ts-extras.d.ts +301 -6
  32. package/lib/packlets/ai-assist/apiClient.d.ts +29 -0
  33. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -1
  34. package/lib/packlets/ai-assist/apiClient.js +30 -25
  35. package/lib/packlets/ai-assist/apiClient.js.map +1 -1
  36. package/lib/packlets/ai-assist/converters.d.ts.map +1 -1
  37. package/lib/packlets/ai-assist/converters.js +2 -1
  38. package/lib/packlets/ai-assist/converters.js.map +1 -1
  39. package/lib/packlets/ai-assist/endpoint.d.ts +28 -0
  40. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
  41. package/lib/packlets/ai-assist/endpoint.js +82 -0
  42. package/lib/packlets/ai-assist/endpoint.js.map +1 -0
  43. package/lib/packlets/ai-assist/index.d.ts +2 -0
  44. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  45. package/lib/packlets/ai-assist/index.js +7 -1
  46. package/lib/packlets/ai-assist/index.js.map +1 -1
  47. package/lib/packlets/ai-assist/jsonCompletion.d.ts +93 -0
  48. package/lib/packlets/ai-assist/jsonCompletion.d.ts.map +1 -0
  49. package/lib/packlets/ai-assist/jsonCompletion.js +99 -0
  50. package/lib/packlets/ai-assist/jsonCompletion.js.map +1 -0
  51. package/lib/packlets/ai-assist/jsonResponse.d.ts +91 -0
  52. package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -0
  53. package/lib/packlets/ai-assist/jsonResponse.js +154 -0
  54. package/lib/packlets/ai-assist/jsonResponse.js.map +1 -0
  55. package/lib/packlets/ai-assist/model.d.ts +9 -1
  56. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  57. package/lib/packlets/ai-assist/model.js.map +1 -1
  58. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  59. package/lib/packlets/ai-assist/registry.js +26 -0
  60. package/lib/packlets/ai-assist/registry.js.map +1 -1
  61. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +8 -0
  62. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -1
  64. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -1
  65. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +2 -1
  66. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -1
  67. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -1
  68. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +2 -1
  69. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -1
  70. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/streamingClient.js +11 -5
  72. package/lib/packlets/ai-assist/streamingClient.js.map +1 -1
  73. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +14 -3
  74. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -1
  75. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +6 -0
  76. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -1
  77. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +43 -1
  78. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -1
  79. package/lib/packlets/crypto-utils/keystore/keyStore.js +81 -0
  80. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -1
  81. package/lib/packlets/crypto-utils/model.d.ts +16 -2
  82. package/lib/packlets/crypto-utils/model.d.ts.map +1 -1
  83. package/lib/packlets/crypto-utils/model.js +2 -1
  84. package/lib/packlets/crypto-utils/model.js.map +1 -1
  85. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +7 -1
  86. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -1
  87. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +20 -1
  88. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -1
  89. package/package.json +7 -7
@@ -0,0 +1,78 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ /**
21
+ * Helper for resolving a request's effective base URL from a provider
22
+ * descriptor and an optional caller-supplied endpoint override.
23
+ *
24
+ * @packageDocumentation
25
+ */
26
+ import { fail, succeed } from '@fgv/ts-utils';
27
+ /**
28
+ * Builds an OpenAI-style `Authorization: Bearer ${key}` header, or an empty
29
+ * record when the key is empty. Self-hosted/local OpenAI-compatible servers
30
+ * (Ollama, LM Studio, llama.cpp) often reject `Authorization: Bearer ` with
31
+ * an empty key, so we omit the header entirely in that case.
32
+ *
33
+ * @internal
34
+ */
35
+ export function bearerAuthHeader(apiKey) {
36
+ return apiKey.length > 0 ? { Authorization: `Bearer ${apiKey}` } : {};
37
+ }
38
+ /**
39
+ * Resolves the effective base URL for a request, validating the optional
40
+ * `endpoint` override when present. Returns the URL with any trailing slash
41
+ * stripped so per-route suffix concatenation (e.g. `/chat/completions`)
42
+ * produces the same shape regardless of whether the caller supplied an
43
+ * override or the descriptor's default is used.
44
+ *
45
+ * @internal
46
+ */
47
+ export function resolveEffectiveBaseUrl(descriptor, endpoint) {
48
+ if (endpoint === undefined) {
49
+ if (!descriptor.baseUrl) {
50
+ return fail(`provider "${descriptor.id}" has no API endpoint configured`);
51
+ }
52
+ return succeed(descriptor.baseUrl.replace(/\/+$/, ''));
53
+ }
54
+ if (typeof endpoint !== 'string' || endpoint.length === 0) {
55
+ return fail(`provider "${descriptor.id}": endpoint must be a non-empty http(s) URL`);
56
+ }
57
+ let parsed;
58
+ try {
59
+ parsed = new URL(endpoint);
60
+ }
61
+ catch (_a) {
62
+ return fail(`provider "${descriptor.id}": endpoint is not a valid URL`);
63
+ }
64
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
65
+ return fail(`provider "${descriptor.id}": endpoint must use http or https (got ${parsed.protocol})`);
66
+ }
67
+ if (parsed.search.length > 0 || parsed.hash.length > 0) {
68
+ return fail(`provider "${descriptor.id}": endpoint must not include a query string or fragment`);
69
+ }
70
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
71
+ return fail(`provider "${descriptor.id}": endpoint must not include userinfo; pass credentials via apiKey instead`);
72
+ }
73
+ // Reconstruct from origin + pathname so the returned URL is normalized
74
+ // (no userinfo, no query, no fragment) and the suffix concat in callers
75
+ // produces a well-formed request URL.
76
+ return succeed(`${parsed.origin}${parsed.pathname}`.replace(/\/+$/, ''));
77
+ }
78
+ //# sourceMappingURL=endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/endpoint.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;AAEZ;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAU,OAAO,EAAE,MAAM,eAAe,CAAC;AAItD;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACxE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,UAAiC,EACjC,QAAiB;IAEjB,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,aAAa,UAAU,CAAC,EAAE,kCAAkC,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,OAAO,IAAI,CAAC,aAAa,UAAU,CAAC,EAAE,6CAA6C,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC,aAAa,UAAU,CAAC,EAAE,gCAAgC,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChE,OAAO,IAAI,CAAC,aAAa,UAAU,CAAC,EAAE,2CAA2C,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvG,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,aAAa,UAAU,CAAC,EAAE,yDAAyD,CAAC,CAAC;IACnG,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7D,OAAO,IAAI,CACT,aAAa,UAAU,CAAC,EAAE,4EAA4E,CACvG,CAAC;IACJ,CAAC;IACD,uEAAuE;IACvE,wEAAwE;IACxE,sCAAsC;IACtC,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3E,CAAC","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Helper for resolving a request's effective base URL from a provider\n * descriptor and an optional caller-supplied endpoint override.\n *\n * @packageDocumentation\n */\n\nimport { fail, Result, succeed } from '@fgv/ts-utils';\n\nimport { type IAiProviderDescriptor } from './model';\n\n/**\n * Builds an OpenAI-style `Authorization: Bearer ${key}` header, or an empty\n * record when the key is empty. Self-hosted/local OpenAI-compatible servers\n * (Ollama, LM Studio, llama.cpp) often reject `Authorization: Bearer ` with\n * an empty key, so we omit the header entirely in that case.\n *\n * @internal\n */\nexport function bearerAuthHeader(apiKey: string): Record<string, string> {\n return apiKey.length > 0 ? { Authorization: `Bearer ${apiKey}` } : {};\n}\n\n/**\n * Resolves the effective base URL for a request, validating the optional\n * `endpoint` override when present. Returns the URL with any trailing slash\n * stripped so per-route suffix concatenation (e.g. `/chat/completions`)\n * produces the same shape regardless of whether the caller supplied an\n * override or the descriptor's default is used.\n *\n * @internal\n */\nexport function resolveEffectiveBaseUrl(\n descriptor: IAiProviderDescriptor,\n endpoint?: string\n): Result<string> {\n if (endpoint === undefined) {\n if (!descriptor.baseUrl) {\n return fail(`provider \"${descriptor.id}\" has no API endpoint configured`);\n }\n return succeed(descriptor.baseUrl.replace(/\\/+$/, ''));\n }\n if (typeof endpoint !== 'string' || endpoint.length === 0) {\n return fail(`provider \"${descriptor.id}\": endpoint must be a non-empty http(s) URL`);\n }\n let parsed: URL;\n try {\n parsed = new URL(endpoint);\n } catch {\n return fail(`provider \"${descriptor.id}\": endpoint is not a valid URL`);\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return fail(`provider \"${descriptor.id}\": endpoint must use http or https (got ${parsed.protocol})`);\n }\n if (parsed.search.length > 0 || parsed.hash.length > 0) {\n return fail(`provider \"${descriptor.id}\": endpoint must not include a query string or fragment`);\n }\n if (parsed.username.length > 0 || parsed.password.length > 0) {\n return fail(\n `provider \"${descriptor.id}\": endpoint must not include userinfo; pass credentials via apiKey instead`\n );\n }\n // Reconstruct from origin + pathname so the returned URL is normalized\n // (no userinfo, no query, no fragment) and the suffix concat in callers\n // produces a well-formed request URL.\n return succeed(`${parsed.origin}${parsed.pathname}`.replace(/\\/+$/, ''));\n}\n"]}
@@ -8,4 +8,6 @@ export { callProviderCompletion, callProxiedCompletion, callProviderImageGenerat
8
8
  export { callProviderCompletionStream, callProxiedCompletionStream } from './streamingClient';
9
9
  export { aiProviderId, aiServerToolType, aiWebSearchToolConfig, aiServerToolConfig, aiToolEnablement, aiAssistProviderConfig, aiAssistSettings, modelSpecKey, modelSpec } from './converters';
10
10
  export { resolveEffectiveTools } from './toolFormats';
11
+ export { extractJsonText, fencedStringifiedJson } from './jsonResponse';
12
+ export { generateJsonCompletion, SMART_JSON_PROMPT_HINT } from './jsonCompletion';
11
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EAeR,iBAAiB,EAmBjB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,SAAS,EACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EAItB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAE5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * AI assist packlet - provider registry, prompt class, settings, and API client.\n * @packageDocumentation\n */\n\nexport {\n AiPrompt,\n type AiModelCapability,\n type AiProviderId,\n type AiServerToolType,\n type AiServerToolConfig,\n type IAiWebSearchToolConfig,\n type IAiToolEnablement,\n type IAiCompletionResponse,\n type IChatMessage,\n type AiApiFormat,\n type AiImageApiFormat,\n type IAiImageModelCapability,\n type IAiProviderDescriptor,\n type IAiAssistProviderConfig,\n type IAiAssistSettings,\n DEFAULT_AI_ASSIST,\n type IAiAssistKeyStore,\n type IAiImageAttachment,\n type IAiImageData,\n type IAiImageGenerationOptions,\n type IAiImageGenerationParams,\n type IAiGeneratedImage,\n type IAiImageGenerationResponse,\n type IAiModelCapabilityRule,\n type IAiModelCapabilityConfig,\n type IAiModelInfo,\n type IAiStreamEvent,\n type IAiStreamTextDelta,\n type IAiStreamToolEvent,\n type IAiStreamDone,\n type IAiStreamError,\n type ModelSpec,\n type ModelSpecKey,\n type IModelSpecMap,\n allModelSpecKeys,\n MODEL_SPEC_BASE_KEY,\n resolveModel,\n toDataUrl\n} from './model';\n\nexport {\n allProviderIds,\n getProviderDescriptors,\n getProviderDescriptor,\n resolveImageCapability,\n supportsImageGeneration,\n DEFAULT_MODEL_CAPABILITY_CONFIG\n} from './registry';\n\nexport {\n callProviderCompletion,\n callProxiedCompletion,\n callProviderImageGeneration,\n callProxiedImageGeneration,\n callProviderListModels,\n callProxiedListModels,\n type IProviderCompletionParams,\n type IProviderImageGenerationParams,\n type IProviderListModelsParams\n} from './apiClient';\n\nexport {\n callProviderCompletionStream,\n callProxiedCompletionStream,\n type IProviderCompletionStreamParams\n} from './streamingClient';\n\nexport {\n aiProviderId,\n aiServerToolType,\n aiWebSearchToolConfig,\n aiServerToolConfig,\n aiToolEnablement,\n aiAssistProviderConfig,\n aiAssistSettings,\n modelSpecKey,\n modelSpec\n} from './converters';\n\nexport { resolveEffectiveTools } from './toolFormats';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EAeR,iBAAiB,EAmBjB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,SAAS,EACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EAItB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAE5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EACL,eAAe,EACf,qBAAqB,EAItB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EAIvB,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * AI assist packlet - provider registry, prompt class, settings, and API client.\n * @packageDocumentation\n */\n\nexport {\n AiPrompt,\n type AiModelCapability,\n type AiProviderId,\n type AiServerToolType,\n type AiServerToolConfig,\n type IAiWebSearchToolConfig,\n type IAiToolEnablement,\n type IAiCompletionResponse,\n type IChatMessage,\n type AiApiFormat,\n type AiImageApiFormat,\n type IAiImageModelCapability,\n type IAiProviderDescriptor,\n type IAiAssistProviderConfig,\n type IAiAssistSettings,\n DEFAULT_AI_ASSIST,\n type IAiAssistKeyStore,\n type IAiImageAttachment,\n type IAiImageData,\n type IAiImageGenerationOptions,\n type IAiImageGenerationParams,\n type IAiGeneratedImage,\n type IAiImageGenerationResponse,\n type IAiModelCapabilityRule,\n type IAiModelCapabilityConfig,\n type IAiModelInfo,\n type IAiStreamEvent,\n type IAiStreamTextDelta,\n type IAiStreamToolEvent,\n type IAiStreamDone,\n type IAiStreamError,\n type ModelSpec,\n type ModelSpecKey,\n type IModelSpecMap,\n allModelSpecKeys,\n MODEL_SPEC_BASE_KEY,\n resolveModel,\n toDataUrl\n} from './model';\n\nexport {\n allProviderIds,\n getProviderDescriptors,\n getProviderDescriptor,\n resolveImageCapability,\n supportsImageGeneration,\n DEFAULT_MODEL_CAPABILITY_CONFIG\n} from './registry';\n\nexport {\n callProviderCompletion,\n callProxiedCompletion,\n callProviderImageGeneration,\n callProxiedImageGeneration,\n callProviderListModels,\n callProxiedListModels,\n type IProviderCompletionParams,\n type IProviderImageGenerationParams,\n type IProviderListModelsParams\n} from './apiClient';\n\nexport {\n callProviderCompletionStream,\n callProxiedCompletionStream,\n type IProviderCompletionStreamParams\n} from './streamingClient';\n\nexport {\n aiProviderId,\n aiServerToolType,\n aiWebSearchToolConfig,\n aiServerToolConfig,\n aiToolEnablement,\n aiAssistProviderConfig,\n aiAssistSettings,\n modelSpecKey,\n modelSpec\n} from './converters';\n\nexport { resolveEffectiveTools } from './toolFormats';\n\nexport {\n extractJsonText,\n fencedStringifiedJson,\n type IFencedStringifiedJsonExtractorOptions,\n type IFencedStringifiedJsonOptions,\n type JsonTextExtractor\n} from './jsonResponse';\n\nexport {\n generateJsonCompletion,\n SMART_JSON_PROMPT_HINT,\n type IGenerateJsonCompletionParams,\n type IGenerateJsonCompletionResult,\n type JsonPromptHint\n} from './jsonCompletion';\n"]}
@@ -0,0 +1,95 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ var __rest = (this && this.__rest) || function (s, e) {
21
+ var t = {};
22
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
23
+ t[p] = s[p];
24
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
25
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
26
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
27
+ t[p[i]] = s[p[i]];
28
+ }
29
+ return t;
30
+ };
31
+ /**
32
+ * `generateJsonCompletion<T>` — request a JSON-shaped completion from any
33
+ * provider supported by AiAssist and validate it against a caller-supplied
34
+ * `Converter<T>` or `Validator<T>`. Wraps fence/preamble tolerance + parsing +
35
+ * validation into a single call so consumers don't reinvent the pipeline.
36
+ *
37
+ * @packageDocumentation
38
+ */
39
+ import { fail, succeed } from '@fgv/ts-utils';
40
+ import { callProviderCompletion } from './apiClient';
41
+ import { fencedStringifiedJson } from './jsonResponse';
42
+ import { AiPrompt } from './model';
43
+ /**
44
+ * Default system-prompt suffix appended when {@link AiAssist.IGenerateJsonCompletionParams.promptHint}
45
+ * is `'smart'` (the default). Designed to discourage code fences and prose in
46
+ * the model's response while still tolerating them via the read-side extractor.
47
+ * @public
48
+ */
49
+ export const SMART_JSON_PROMPT_HINT = 'Respond with raw JSON only — no Markdown code fences, no explanatory prose, ' +
50
+ 'no preamble or trailing commentary. The response must parse with JSON.parse.';
51
+ function applyPromptHint(prompt, hint) {
52
+ if (hint === 'none') {
53
+ return prompt;
54
+ }
55
+ const suffix = hint === 'smart' ? SMART_JSON_PROMPT_HINT : hint;
56
+ const system = prompt.system.length > 0 ? `${prompt.system}\n\n${suffix}` : suffix;
57
+ return new AiPrompt(prompt.user, system, prompt.attachments);
58
+ }
59
+ /**
60
+ * Calls {@link AiAssist.callProviderCompletion}, then runs the response text
61
+ * through a tolerant JSON converter (default:
62
+ * {@link AiAssist.fencedStringifiedJson}) and the caller's
63
+ * `converter`/`validator`. Returns the validated value plus the raw text and
64
+ * underlying completion response for diagnostics.
65
+ *
66
+ * @remarks
67
+ * The default smart prompt hint asks the model to emit raw JSON. The read-side
68
+ * extractor still tolerates fences and prose, so models that ignore the hint
69
+ * are still handled.
70
+ *
71
+ * Either `converter` or `jsonConverter` must be provided; passing both lets
72
+ * `jsonConverter` win.
73
+ *
74
+ * @param params - Provider parameters plus JSON validation options.
75
+ * @returns The validated value, the raw text, and the underlying response.
76
+ * @public
77
+ */
78
+ export async function generateJsonCompletion(params) {
79
+ const { converter, jsonConverter, promptHint = 'smart', prompt } = params, rest = __rest(params, ["converter", "jsonConverter", "promptHint", "prompt"]);
80
+ if (jsonConverter === undefined && converter === undefined) {
81
+ return fail('generateJsonCompletion: either converter or jsonConverter must be provided.');
82
+ }
83
+ const pipeline = jsonConverter !== null && jsonConverter !== void 0 ? jsonConverter : fencedStringifiedJson({ inner: converter });
84
+ const augmentedPrompt = applyPromptHint(prompt, promptHint);
85
+ const response = await callProviderCompletion(Object.assign(Object.assign({}, rest), { prompt: augmentedPrompt }));
86
+ if (response.isFailure()) {
87
+ return fail(response.message);
88
+ }
89
+ const completion = response.value;
90
+ return pipeline
91
+ .convert(completion.content)
92
+ .withErrorFormat((msg) => `generateJsonCompletion: ${msg}`)
93
+ .onSuccess((value) => succeed({ value, raw: completion.content, response: completion }));
94
+ }
95
+ //# sourceMappingURL=jsonCompletion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonCompletion.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/jsonCompletion.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;;;;;;;;;;;;AAEZ;;;;;;;GAOG;AAEH,OAAO,EAAkB,IAAI,EAAU,OAAO,EAAkB,MAAM,eAAe,CAAC;AAEtF,OAAO,EAAE,sBAAsB,EAAkC,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAA8B,MAAM,SAAS,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GACjC,8EAA8E;IAC9E,8EAA8E,CAAC;AA6DjF,SAAS,eAAe,CAAC,MAAgB,EAAE,IAAoB;IAC7D,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC;IAChE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IACnF,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAwC;IAExC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,GAAG,OAAO,EAAE,MAAM,KAAc,MAAM,EAAf,IAAI,UAAK,MAAM,EAA5E,sDAAmE,CAAS,CAAC;IAEnF,IAAI,aAAa,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC3D,OAAO,IAAI,CAAC,6EAA6E,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,QAAQ,GAAiB,aAAa,aAAb,aAAa,cAAb,aAAa,GAAI,qBAAqB,CAAI,EAAE,KAAK,EAAE,SAAU,EAAE,CAAC,CAAC;IAChG,MAAM,eAAe,GAAG,eAAe,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,MAAM,sBAAsB,iCAAM,IAAI,KAAE,MAAM,EAAE,eAAe,IAAG,CAAC;IACpF,IAAI,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC;IAElC,OAAO,QAAQ;SACZ,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;SAC3B,eAAe,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,2BAA2B,GAAG,EAAE,CAAC;SAC1D,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;AAC7F,CAAC","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * `generateJsonCompletion<T>` — request a JSON-shaped completion from any\n * provider supported by AiAssist and validate it against a caller-supplied\n * `Converter<T>` or `Validator<T>`. Wraps fence/preamble tolerance + parsing +\n * validation into a single call so consumers don't reinvent the pipeline.\n *\n * @packageDocumentation\n */\n\nimport { type Converter, fail, Result, succeed, type Validator } from '@fgv/ts-utils';\n\nimport { callProviderCompletion, type IProviderCompletionParams } from './apiClient';\nimport { fencedStringifiedJson } from './jsonResponse';\nimport { AiPrompt, type IAiCompletionResponse } from './model';\n\n/**\n * Default system-prompt suffix appended when {@link AiAssist.IGenerateJsonCompletionParams.promptHint}\n * is `'smart'` (the default). Designed to discourage code fences and prose in\n * the model's response while still tolerating them via the read-side extractor.\n * @public\n */\nexport const SMART_JSON_PROMPT_HINT: string =\n 'Respond with raw JSON only — no Markdown code fences, no explanatory prose, ' +\n 'no preamble or trailing commentary. The response must parse with JSON.parse.';\n\n/**\n * Controls the optional system-prompt augmentation applied by\n * {@link AiAssist.generateJsonCompletion}.\n *\n * - `'smart'` (default): append {@link AiAssist.SMART_JSON_PROMPT_HINT}.\n * - `'none'`: do not modify the prompt.\n * - A string: append the supplied text verbatim.\n *\n * @remarks\n * The `string & {}` branch is the standard TypeScript trick that prevents\n * the literal members from being widened away — callers still get\n * autocomplete for `'smart'` and `'none'` while accepting any string.\n *\n * @public\n */\nexport type JsonPromptHint = 'smart' | 'none' | (string & {});\n\n/**\n * Parameters for {@link AiAssist.generateJsonCompletion}. Extends\n * {@link AiAssist.IProviderCompletionParams} with JSON-validation knobs.\n * @public\n */\nexport interface IGenerateJsonCompletionParams<T> extends IProviderCompletionParams {\n /**\n * Caller-supplied `Converter<T>` or `Validator<T>` applied to the parsed\n * JSON value. Wrapped internally in {@link AiAssist.fencedStringifiedJson}\n * unless {@link AiAssist.IGenerateJsonCompletionParams.jsonConverter} is\n * provided.\n */\n readonly converter?: Converter<T> | Validator<T>;\n\n /**\n * Full string-to-`T` pipeline override. When supplied, takes precedence over\n * {@link AiAssist.IGenerateJsonCompletionParams.converter} and lets the\n * caller plug in a custom extractor or skip the default fence tolerance\n * entirely.\n */\n readonly jsonConverter?: Converter<T>;\n\n /**\n * Controls the optional system-prompt augmentation. Defaults to `'smart'`.\n * Pass `'none'` to disable, or a string to append custom guidance.\n */\n readonly promptHint?: JsonPromptHint;\n}\n\n/**\n * Successful result of {@link AiAssist.generateJsonCompletion}.\n * @public\n */\nexport interface IGenerateJsonCompletionResult<T> {\n /** The validated JSON value. */\n readonly value: T;\n /** The raw response text returned by the provider. */\n readonly raw: string;\n /** The full underlying completion response. */\n readonly response: IAiCompletionResponse;\n}\n\nfunction applyPromptHint(prompt: AiPrompt, hint: JsonPromptHint): AiPrompt {\n if (hint === 'none') {\n return prompt;\n }\n const suffix = hint === 'smart' ? SMART_JSON_PROMPT_HINT : hint;\n const system = prompt.system.length > 0 ? `${prompt.system}\\n\\n${suffix}` : suffix;\n return new AiPrompt(prompt.user, system, prompt.attachments);\n}\n\n/**\n * Calls {@link AiAssist.callProviderCompletion}, then runs the response text\n * through a tolerant JSON converter (default:\n * {@link AiAssist.fencedStringifiedJson}) and the caller's\n * `converter`/`validator`. Returns the validated value plus the raw text and\n * underlying completion response for diagnostics.\n *\n * @remarks\n * The default smart prompt hint asks the model to emit raw JSON. The read-side\n * extractor still tolerates fences and prose, so models that ignore the hint\n * are still handled.\n *\n * Either `converter` or `jsonConverter` must be provided; passing both lets\n * `jsonConverter` win.\n *\n * @param params - Provider parameters plus JSON validation options.\n * @returns The validated value, the raw text, and the underlying response.\n * @public\n */\nexport async function generateJsonCompletion<T>(\n params: IGenerateJsonCompletionParams<T>\n): Promise<Result<IGenerateJsonCompletionResult<T>>> {\n const { converter, jsonConverter, promptHint = 'smart', prompt, ...rest } = params;\n\n if (jsonConverter === undefined && converter === undefined) {\n return fail('generateJsonCompletion: either converter or jsonConverter must be provided.');\n }\n\n const pipeline: Converter<T> = jsonConverter ?? fencedStringifiedJson<T>({ inner: converter! });\n const augmentedPrompt = applyPromptHint(prompt, promptHint);\n\n const response = await callProviderCompletion({ ...rest, prompt: augmentedPrompt });\n if (response.isFailure()) {\n return fail(response.message);\n }\n const completion = response.value;\n\n return pipeline\n .convert(completion.content)\n .withErrorFormat((msg) => `generateJsonCompletion: ${msg}`)\n .onSuccess((value) => succeed({ value, raw: completion.content, response: completion }));\n}\n"]}
@@ -0,0 +1,149 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ /**
21
+ * JSON-tolerant extraction and converters for LLM responses.
22
+ *
23
+ * Models commonly wrap JSON output in Markdown code fences, add a
24
+ * "Sure, here's the JSON:" preamble, or trail off with prose after the
25
+ * closing brace. These helpers normalize that quirk on the read side so every
26
+ * AiAssist consumer can reach a validated `T` from raw model text without
27
+ * reimplementing the same fence-stripping logic.
28
+ *
29
+ * Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair
30
+ * malformed JSON (missing commas, unquoted keys, smart quotes, etc.).
31
+ *
32
+ * @packageDocumentation
33
+ */
34
+ import { Conversion, fail, succeed } from '@fgv/ts-utils';
35
+ import { Converters as JsonBaseConverters } from '@fgv/ts-json-base';
36
+ const FENCED_BLOCK = /```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n?```/;
37
+ const BOM = /^\uFEFF/;
38
+ // Full RFC 8259 grammar so the extractor only succeeds when the entire
39
+ // candidate parses as a JSON primitive (instead of just starting like one).
40
+ const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
41
+ // eslint-disable-next-line no-control-regex
42
+ const JSON_STRING = /^"(?:[^"\\\u0000-\u001F]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"$/;
43
+ const JSON_KEYWORD = /^(?:true|false|null)$/;
44
+ function stripBom(text) {
45
+ return text.replace(BOM, '');
46
+ }
47
+ function findBalancedJsonSubstring(text) {
48
+ // Walk the text once tracking string state. The first '{' or '[' that is
49
+ // *outside* a quoted string is the candidate start; from there, count
50
+ // matching close characters while ignoring delimiters that appear inside
51
+ // strings.
52
+ let inString = false;
53
+ let escape = false;
54
+ let start = -1;
55
+ let open = '';
56
+ let close = '';
57
+ let depth = 0;
58
+ for (let i = 0; i < text.length; i++) {
59
+ const ch = text.charAt(i);
60
+ if (inString) {
61
+ if (escape) {
62
+ escape = false;
63
+ }
64
+ else if (ch === '\\') {
65
+ escape = true;
66
+ }
67
+ else if (ch === '"') {
68
+ inString = false;
69
+ }
70
+ continue;
71
+ }
72
+ if (ch === '"') {
73
+ inString = true;
74
+ continue;
75
+ }
76
+ if (start < 0) {
77
+ if (ch === '{' || ch === '[') {
78
+ start = i;
79
+ open = ch;
80
+ close = ch === '{' ? '}' : ']';
81
+ depth = 1;
82
+ }
83
+ continue;
84
+ }
85
+ if (ch === open) {
86
+ depth++;
87
+ }
88
+ else if (ch === close) {
89
+ depth--;
90
+ if (depth === 0) {
91
+ return text.slice(start, i + 1);
92
+ }
93
+ }
94
+ }
95
+ return undefined;
96
+ }
97
+ /**
98
+ * Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:
99
+ *
100
+ * - Leading/trailing whitespace and a leading byte-order mark.
101
+ * - Markdown code fences (with or without a language tag).
102
+ * - Conversational preamble before the first `{` or `[`.
103
+ * - Trailing prose after the matched closing `}` or `]`.
104
+ *
105
+ * Out of scope: repairing malformed JSON, handling smart quotes, etc.
106
+ *
107
+ * @param text - Raw model output.
108
+ * @returns A `Result<string>` containing the JSON-shaped substring, or a
109
+ * `Failure` if no JSON-shaped substring was found.
110
+ * @public
111
+ */
112
+ export const extractJsonText = (text) => {
113
+ if (typeof text !== 'string') {
114
+ return fail('extractJsonText: input must be a string.');
115
+ }
116
+ const stripped = stripBom(text).trim();
117
+ if (stripped.length === 0) {
118
+ return fail('extractJsonText: input is empty.');
119
+ }
120
+ const fenced = FENCED_BLOCK.exec(stripped);
121
+ const candidate = fenced ? fenced[1].trim() : stripped;
122
+ if (candidate.length === 0) {
123
+ return fail('extractJsonText: no JSON content found.');
124
+ }
125
+ // Whole-candidate primitive check runs before the brace scan so that a
126
+ // valid JSON string containing braces (e.g. `"text with { }"`) is returned
127
+ // intact instead of being mangled into the first balanced `{ }` match.
128
+ if (JSON_KEYWORD.test(candidate) || JSON_NUMBER.test(candidate) || JSON_STRING.test(candidate)) {
129
+ return succeed(candidate);
130
+ }
131
+ const balanced = findBalancedJsonSubstring(candidate);
132
+ if (balanced !== undefined) {
133
+ return succeed(balanced);
134
+ }
135
+ return fail('extractJsonText: no JSON-shaped substring found.');
136
+ };
137
+ export function fencedStringifiedJson(options) {
138
+ var _a;
139
+ const extractor = (_a = options === null || options === void 0 ? void 0 : options.extractor) !== null && _a !== void 0 ? _a : extractJsonText;
140
+ const inner = options === null || options === void 0 ? void 0 : options.inner;
141
+ const parser = inner !== undefined ? JsonBaseConverters.stringifiedJson(inner) : JsonBaseConverters.stringifiedJson();
142
+ return new Conversion.BaseConverter((from) => {
143
+ if (typeof from !== 'string') {
144
+ return fail('fencedStringifiedJson: input must be a string.');
145
+ }
146
+ return extractor(from).onSuccess((extracted) => parser.convert(extracted));
147
+ });
148
+ }
149
+ //# sourceMappingURL=jsonResponse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/jsonResponse.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;AAEZ;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAkB,IAAI,EAAU,OAAO,EAAkB,MAAM,eAAe,CAAC;AAClG,OAAO,EAAE,UAAU,IAAI,kBAAkB,EAAkB,MAAM,mBAAmB,CAAC;AAUrF,MAAM,YAAY,GAAW,8CAA8C,CAAC;AAC5E,MAAM,GAAG,GAAW,SAAS,CAAC;AAC9B,uEAAuE;AACvE,4EAA4E;AAC5E,MAAM,WAAW,GAAW,+CAA+C,CAAC;AAC5E,4CAA4C;AAC5C,MAAM,WAAW,GAAW,gEAAgE,CAAC;AAC7F,MAAM,YAAY,GAAW,uBAAuB,CAAC;AAErD,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,yBAAyB,CAAC,IAAY;IAC7C,yEAAyE;IACzE,sEAAsE;IACtE,yEAAyE;IACzE,WAAW;IACX,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;IACf,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,GAAG,KAAK,CAAC;YACjB,CAAC;iBAAM,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBACvB,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;iBAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACtB,QAAQ,GAAG,KAAK,CAAC;YACnB,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;QACD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBAC7B,KAAK,GAAG,CAAC,CAAC;gBACV,IAAI,GAAG,EAAE,CAAC;gBACV,KAAK,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;gBAC/B,KAAK,GAAG,CAAC,CAAC;YACZ,CAAC;YACD,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,KAAK,EAAE,CAAC;QACV,CAAC;aAAM,IAAI,EAAE,KAAK,KAAK,EAAE,CAAC;YACxB,KAAK,EAAE,CAAC;YACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBAChB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,IAAY,EAAkB,EAAE;IACjF,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,kCAAkC,CAAC,CAAC;IAClD,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;IAEvD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,yCAAyC,CAAC,CAAC;IACzD,CAAC;IAED,uEAAuE;IACvE,2EAA2E;IAC3E,uEAAuE;IACvE,IAAI,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/F,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,QAAQ,GAAG,yBAAyB,CAAC,SAAS,CAAC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,IAAI,CAAC,kDAAkD,CAAC,CAAC;AAClE,CAAC,CAAC;AAuDF,MAAM,UAAU,qBAAqB,CACnC,OAAmF;;IAEnF,MAAM,SAAS,GAAsB,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,mCAAI,eAAe,CAAC;IAC3E,MAAM,KAAK,GAAI,OAAwD,aAAxD,OAAO,uBAAP,OAAO,CAAmD,KAAK,CAAC;IAC/E,MAAM,MAAM,GACV,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,eAAe,CAAI,KAAK,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,eAAe,EAAE,CAAC;IAE5G,OAAO,IAAI,UAAU,CAAC,aAAa,CAAgB,CAAC,IAAa,EAAyB,EAAE;QAC1F,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,gDAAgD,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * JSON-tolerant extraction and converters for LLM responses.\n *\n * Models commonly wrap JSON output in Markdown code fences, add a\n * \"Sure, here's the JSON:\" preamble, or trail off with prose after the\n * closing brace. These helpers normalize that quirk on the read side so every\n * AiAssist consumer can reach a validated `T` from raw model text without\n * reimplementing the same fence-stripping logic.\n *\n * Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair\n * malformed JSON (missing commas, unquoted keys, smart quotes, etc.).\n *\n * @packageDocumentation\n */\n\nimport { Conversion, type Converter, fail, Result, succeed, type Validator } from '@fgv/ts-utils';\nimport { Converters as JsonBaseConverters, type JsonValue } from '@fgv/ts-json-base';\n\n/**\n * A function that pulls a JSON-shaped substring out of arbitrary model text.\n * Implementations strip whatever wrappers the model added (fences, preamble,\n * trailing prose) and return the JSON-shaped substring ready for `JSON.parse`.\n * @public\n */\nexport type JsonTextExtractor = (text: string) => Result<string>;\n\nconst FENCED_BLOCK: RegExp = /```[A-Za-z0-9_-]*\\s*\\r?\\n([\\s\\S]*?)\\r?\\n?```/;\nconst BOM: RegExp = /^\\uFEFF/;\n// Full RFC 8259 grammar so the extractor only succeeds when the entire\n// candidate parses as a JSON primitive (instead of just starting like one).\nconst JSON_NUMBER: RegExp = /^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/;\n// eslint-disable-next-line no-control-regex\nconst JSON_STRING: RegExp = /^\"(?:[^\"\\\\\\u0000-\\u001F]|\\\\(?:[\"\\\\/bfnrt]|u[0-9a-fA-F]{4}))*\"$/;\nconst JSON_KEYWORD: RegExp = /^(?:true|false|null)$/;\n\nfunction stripBom(text: string): string {\n return text.replace(BOM, '');\n}\n\nfunction findBalancedJsonSubstring(text: string): string | undefined {\n // Walk the text once tracking string state. The first '{' or '[' that is\n // *outside* a quoted string is the candidate start; from there, count\n // matching close characters while ignoring delimiters that appear inside\n // strings.\n let inString = false;\n let escape = false;\n let start = -1;\n let open = '';\n let close = '';\n let depth = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n if (inString) {\n if (escape) {\n escape = false;\n } else if (ch === '\\\\') {\n escape = true;\n } else if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n if (ch === '\"') {\n inString = true;\n continue;\n }\n if (start < 0) {\n if (ch === '{' || ch === '[') {\n start = i;\n open = ch;\n close = ch === '{' ? '}' : ']';\n depth = 1;\n }\n continue;\n }\n if (ch === open) {\n depth++;\n } else if (ch === close) {\n depth--;\n if (depth === 0) {\n return text.slice(start, i + 1);\n }\n }\n }\n return undefined;\n}\n\n/**\n * Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:\n *\n * - Leading/trailing whitespace and a leading byte-order mark.\n * - Markdown code fences (with or without a language tag).\n * - Conversational preamble before the first `{` or `[`.\n * - Trailing prose after the matched closing `}` or `]`.\n *\n * Out of scope: repairing malformed JSON, handling smart quotes, etc.\n *\n * @param text - Raw model output.\n * @returns A `Result<string>` containing the JSON-shaped substring, or a\n * `Failure` if no JSON-shaped substring was found.\n * @public\n */\nexport const extractJsonText: JsonTextExtractor = (text: string): Result<string> => {\n if (typeof text !== 'string') {\n return fail('extractJsonText: input must be a string.');\n }\n const stripped = stripBom(text).trim();\n if (stripped.length === 0) {\n return fail('extractJsonText: input is empty.');\n }\n\n const fenced = FENCED_BLOCK.exec(stripped);\n const candidate = fenced ? fenced[1].trim() : stripped;\n\n if (candidate.length === 0) {\n return fail('extractJsonText: no JSON content found.');\n }\n\n // Whole-candidate primitive check runs before the brace scan so that a\n // valid JSON string containing braces (e.g. `\"text with { }\"`) is returned\n // intact instead of being mangled into the first balanced `{ }` match.\n if (JSON_KEYWORD.test(candidate) || JSON_NUMBER.test(candidate) || JSON_STRING.test(candidate)) {\n return succeed(candidate);\n }\n\n const balanced = findBalancedJsonSubstring(candidate);\n if (balanced !== undefined) {\n return succeed(balanced);\n }\n\n return fail('extractJsonText: no JSON-shaped substring found.');\n};\n\n/**\n * Options shared by every {@link AiAssist.fencedStringifiedJson} call.\n * @public\n */\nexport interface IFencedStringifiedJsonExtractorOptions {\n /**\n * Optional pre-parse extractor. Defaults to {@link AiAssist.extractJsonText}.\n * Provide a custom extractor to handle response shapes the default does not\n * understand.\n */\n readonly extractor?: JsonTextExtractor;\n}\n\n/**\n * Options for the validating overload of {@link AiAssist.fencedStringifiedJson}.\n * `inner` is required so the typed `Converter<T>` return value can never lie\n * about the runtime shape.\n * @public\n */\nexport interface IFencedStringifiedJsonOptions<T> extends IFencedStringifiedJsonExtractorOptions {\n /** Inner converter or validator applied to the parsed JSON value. */\n readonly inner: Converter<T> | Validator<T>;\n}\n\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies an optional inner converter or\n * validator.\n *\n * @example\n * ```ts\n * const converter = fencedStringifiedJson({ inner: myShapeConverter });\n * const result = converter.convert(llmText); // Result<MyShape>\n * ```\n *\n * @param options - Optional extractor; omit to keep the default. Without an\n * `inner` step, the converter resolves to the parsed `JsonValue`.\n * @returns A `Converter<JsonValue>`.\n * @public\n */\nexport function fencedStringifiedJson(options?: IFencedStringifiedJsonExtractorOptions): Converter<JsonValue>;\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies the supplied inner converter or\n * validator.\n *\n * @param options - Required `inner` converter/validator and optional extractor.\n * @returns A `Converter<T>`.\n * @public\n */\nexport function fencedStringifiedJson<T>(options: IFencedStringifiedJsonOptions<T>): Converter<T>;\nexport function fencedStringifiedJson<T>(\n options?: IFencedStringifiedJsonExtractorOptions | IFencedStringifiedJsonOptions<T>\n): Converter<T | JsonValue> {\n const extractor: JsonTextExtractor = options?.extractor ?? extractJsonText;\n const inner = (options as IFencedStringifiedJsonOptions<T> | undefined)?.inner;\n const parser: Converter<T | JsonValue> =\n inner !== undefined ? JsonBaseConverters.stringifiedJson<T>(inner) : JsonBaseConverters.stringifiedJson();\n\n return new Conversion.BaseConverter<T | JsonValue>((from: unknown): Result<T | JsonValue> => {\n if (typeof from !== 'string') {\n return fail('fencedStringifiedJson: input must be a string.');\n }\n return extractor(from).onSuccess((extracted) => parser.convert(extracted));\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/model.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;AA8BZ;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAmB;IAC3C,OAAO,QAAQ,KAAK,CAAC,QAAQ,WAAW,KAAK,CAAC,MAAM,EAAE,CAAC;AACzD,CAAC;AAuBD,+EAA+E;AAC/E,WAAW;AACX,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,OAAO,QAAQ;IAYnB,YAAmB,IAAY,EAAE,MAAc,EAAE,WAA+C;QAC9F,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,aAAX,WAAW,cAAX,WAAW,GAAI,EAAE,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,IAAW,QAAQ;QACjB,MAAM,QAAQ,GACZ,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC;YACzB,CAAC,CAAC,QAAQ,IAAI,CAAC,WAAW,CAAC,MAAM,qDAAqD;YACtF,CAAC,CAAC,EAAE,CAAC;QACT,OAAO,GAAG,IAAI,CAAC,IAAI,GAAG,QAAQ,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;IACrD,CAAC;CACF;AA6ED;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAgC,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAExF;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAiB,MAAM,CAAC;AAiCxD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,YAAY,CAAC,IAAe,EAAE,OAAgB;IAC5D,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sCAAsC;IACtC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QAC7C,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,sBAAsB;IACtB,IAAI,mBAAmB,IAAI,IAAI,EAAE,CAAC;QAChC,OAAO,YAAY,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,yCAAyC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,6FAA6F;IAC7F,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAiZD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;CACxC,CAAC","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Core types for AI assist: prompt class, provider descriptors, settings, and chat messages.\n * @packageDocumentation\n */\n\nimport { type Result } from '@fgv/ts-utils';\n\n// ============================================================================\n// Image Data\n// ============================================================================\n\n/**\n * Universal image representation used for both image input (vision prompts)\n * and image output (generation responses).\n *\n * @remarks\n * The base64 string is raw — no `data:` URL prefix. Use {@link AiAssist.toDataUrl} to\n * format it for browser-display contexts.\n *\n * @public\n */\nexport interface IAiImageData {\n /** MIME type, e.g. `'image/png'`, `'image/jpeg'`, `'image/webp'`. */\n readonly mimeType: string;\n /** Base64-encoded image bytes (no `data:` prefix). */\n readonly base64: string;\n}\n\n/**\n * Formats an {@link IAiImageData} as a `data:` URL suitable for browser display.\n * @param image - The image to format\n * @returns A `data:<mime>;base64,<data>` URL string\n * @public\n */\nexport function toDataUrl(image: IAiImageData): string {\n return `data:${image.mimeType};base64,${image.base64}`;\n}\n\n/**\n * Image attachment for a vision (image-input) prompt.\n *\n * @remarks\n * Extends {@link IAiImageData} with an OpenAI-specific `detail` hint that is\n * silently ignored by Anthropic, Gemini, and other providers.\n *\n * @public\n */\nexport interface IAiImageAttachment extends IAiImageData {\n /**\n * OpenAI vision detail hint:\n * - `'low'`: faster, cheaper, lower fidelity\n * - `'high'`: slower, more expensive, higher fidelity\n * - `'auto'` (default): provider chooses\n *\n * Ignored by providers other than OpenAI.\n */\n readonly detail?: 'low' | 'high' | 'auto';\n}\n\n// ============================================================================\n// AiPrompt\n// ============================================================================\n\n/**\n * A structured AI prompt with system/user split for direct API calls,\n * and a lazily-constructed combined version for copy/paste workflows.\n * @public\n */\nexport class AiPrompt {\n /** System instructions: schema documentation, format rules, general guidance. */\n public readonly system: string;\n /** User request: the specific entity generation request. */\n public readonly user: string;\n /**\n * Optional image attachments. When present, vision-capable providers will\n * include them in the user message; non-vision providers will reject the\n * call up front (see {@link AiAssist.IAiProviderDescriptor.acceptsImageInput}).\n */\n public readonly attachments: ReadonlyArray<IAiImageAttachment>;\n\n public constructor(user: string, system: string, attachments?: ReadonlyArray<IAiImageAttachment>) {\n this.system = system;\n this.user = user;\n this.attachments = attachments ?? [];\n }\n\n /**\n * Combined single-string version (user + system joined) for copy/paste.\n * When attachments are present, includes a sentinel noting they aren't\n * part of the copied text.\n */\n public get combined(): string {\n const sentinel =\n this.attachments.length > 0\n ? `\\n\\n[${this.attachments.length} image attachment(s) — not included in copied text]`\n : '';\n return `${this.user}${sentinel}\\n\\n${this.system}`;\n }\n}\n\n// ============================================================================\n// Chat Message\n// ============================================================================\n\n/**\n * A single chat message in OpenAI format.\n * @public\n */\nexport interface IChatMessage {\n /** Message role */\n readonly role: 'system' | 'user' | 'assistant';\n /** Message content */\n readonly content: string;\n}\n\n// ============================================================================\n// Server-Side Tools\n// ============================================================================\n\n/**\n * Built-in server-side tool types supported across providers.\n * @public\n */\nexport type AiServerToolType = 'web_search';\n\n/**\n * Configuration specific to web search tools.\n * @public\n */\nexport interface IAiWebSearchToolConfig {\n readonly type: 'web_search';\n /** Optional: restrict search to these domains. */\n readonly allowedDomains?: ReadonlyArray<string>;\n /** Optional: exclude these domains from search. */\n readonly blockedDomains?: ReadonlyArray<string>;\n /** Optional: max number of searches per request. */\n readonly maxUses?: number;\n /**\n * Optional: enable image understanding during web search.\n * When true, the model can view and analyze images found during search.\n * Currently supported by xAI only; ignored by other providers.\n */\n readonly enableImageUnderstanding?: boolean;\n}\n\n/**\n * Union of all server-side tool configurations. Discriminated on `type`.\n * @public\n */\nexport type AiServerToolConfig = IAiWebSearchToolConfig;\n\n/**\n * Declares a tool as enabled/disabled in provider settings.\n * Tools are disabled by default — consuming apps must opt in explicitly.\n * @public\n */\nexport interface IAiToolEnablement {\n /** Which tool type. */\n readonly type: AiServerToolType;\n /** Whether this tool is enabled by default for this provider. */\n readonly enabled: boolean;\n /** Optional tool-specific configuration. */\n readonly config?: AiServerToolConfig;\n}\n\n// ============================================================================\n// Model Specification\n// ============================================================================\n\n/**\n * Known context keys for model specification maps.\n * @public\n */\nexport type ModelSpecKey = 'base' | 'tools' | 'image';\n\n/**\n * All valid {@link ModelSpecKey} values.\n * @public\n */\nexport const allModelSpecKeys: ReadonlyArray<ModelSpecKey> = ['base', 'tools', 'image'];\n\n/**\n * Default context key used as fallback when resolving a {@link ModelSpec}.\n * @public\n */\nexport const MODEL_SPEC_BASE_KEY: ModelSpecKey = 'base';\n\n/**\n * A model specification: either a simple model string or a record mapping\n * context keys to nested model specs.\n *\n * @remarks\n * A bare string is equivalent to `{ base: string }`. This keeps the simple\n * case simple while allowing context-aware model selection (e.g. different\n * models for tool-augmented vs. base completions).\n *\n * @example\n * ```typescript\n * // Simple — same model for all contexts:\n * const simple: ModelSpec = 'grok-4-1-fast';\n *\n * // Context-aware — reasoning model when tools are active:\n * const split: ModelSpec = { base: 'grok-4-1-fast', tools: 'grok-4-1-fast-reasoning' };\n *\n * // Future nested — per-tool model selection:\n * const nested: ModelSpec = { base: 'grok-fast', tools: { base: 'grok-r', image: 'grok-v' } };\n * ```\n * @public\n */\nexport interface IModelSpecMap {\n readonly [key: string]: ModelSpec;\n}\n\n/**\n * @public\n */\nexport type ModelSpec = string | IModelSpecMap;\n\n/**\n * Resolves a {@link ModelSpec} to a concrete model string given an optional context key.\n *\n * @remarks\n * Resolution rules:\n * 1. If the spec is a string, return it directly (context is irrelevant).\n * 2. If the spec is an object and the context key exists, recurse into that branch.\n * 3. Otherwise, fall back to the {@link MODEL_SPEC_BASE_KEY | 'base'} key.\n * 4. If neither context nor `'base'` exists, use the first available value.\n *\n * @param spec - The model specification to resolve\n * @param context - Optional context key (e.g. `'tools'`)\n * @returns The resolved model string\n * @public\n */\nexport function resolveModel(spec: ModelSpec, context?: string): string {\n if (typeof spec === 'string') {\n return spec;\n }\n\n // Try the requested context key first\n if (context !== undefined && context in spec) {\n return resolveModel(spec[context]);\n }\n\n // Fall back to 'base'\n if (MODEL_SPEC_BASE_KEY in spec) {\n return resolveModel(spec[MODEL_SPEC_BASE_KEY]);\n }\n\n // Last resort: first value in the record\n const first = Object.values(spec)[0];\n /* c8 ignore next 3 - defensive: only reachable with empty object (prevented by converter) */\n if (first === undefined) {\n return '';\n }\n return resolveModel(first);\n}\n\n// ============================================================================\n// Provider Descriptor\n// ============================================================================\n\n/**\n * All known AI provider identifiers.\n * @public\n */\nexport type AiProviderId =\n | 'copy-paste'\n | 'xai-grok'\n | 'openai'\n | 'anthropic'\n | 'google-gemini'\n | 'groq'\n | 'mistral';\n\n/**\n * API format categories for provider routing.\n * @public\n */\nexport type AiApiFormat = 'openai' | 'anthropic' | 'gemini';\n\n/**\n * API format categories for image-generation provider routing.\n *\n * @remarks\n * - `'openai-images'` — OpenAI Images API. Routes to `/images/generations`\n * (text-only) or `/images/edits` (when reference images are present).\n * - `'xai-images'` — xAI Images API. Same wire shape as OpenAI but text-only;\n * no reference-image support on grok-2-image.\n * - `'gemini-imagen'` — Google Imagen `:predict` endpoint. Text-only.\n * - `'gemini-image-out'` — Google Gemini chat-style `:generateContent`\n * endpoint that returns image parts (Gemini 2.5 Flash Image / \"Nano\n * Banana\"). Accepts reference images.\n *\n * @public\n */\nexport type AiImageApiFormat = 'openai-images' | 'gemini-imagen' | 'xai-images' | 'gemini-image-out';\n\n// ============================================================================\n// Completion Response\n// ============================================================================\n\n/**\n * Result of an AI provider completion call.\n * @public\n */\nexport interface IAiCompletionResponse {\n /** The generated text content */\n readonly content: string;\n /** Whether the response was truncated due to token limits */\n readonly truncated: boolean;\n}\n\n// ============================================================================\n// Streaming Events\n// ============================================================================\n\n/**\n * A text-content delta arriving during a streaming completion.\n * @public\n */\nexport interface IAiStreamTextDelta {\n readonly type: 'text-delta';\n /** The newly arrived text fragment. */\n readonly delta: string;\n}\n\n/**\n * A server-side tool progress event arriving during a streaming completion.\n * Surfaced for providers that emit explicit tool-progress markers (OpenAI\n * Responses API, Anthropic). Gemini's grounding doesn't emit these.\n * @public\n */\nexport interface IAiStreamToolEvent {\n readonly type: 'tool-event';\n /** Which server-side tool this event describes. */\n readonly toolType: AiServerToolType;\n /** Tool lifecycle phase. */\n readonly phase: 'started' | 'completed';\n /**\n * Optional provider-specific detail. For web_search this is typically the\n * search query when available; format varies by provider.\n */\n readonly detail?: string;\n}\n\n/**\n * Terminal success event for a streaming completion. Carries the aggregated\n * full text and truncation status for callers that want both the progressive\n * UI and the complete result.\n * @public\n */\nexport interface IAiStreamDone {\n readonly type: 'done';\n /** Whether the response was truncated due to token limits. */\n readonly truncated: boolean;\n /** The full concatenated text from all `text-delta` events. */\n readonly fullText: string;\n}\n\n/**\n * Terminal failure event for a streaming completion. After this event no\n * further events are emitted.\n *\n * @remarks\n * Connection-time failures (auth, network, pre-flight CORS rejection) are\n * surfaced via the outer `Result.fail` returned by\n * `callProviderCompletionStream` rather than as an `error` event, so callers\n * can distinguish \"didn't start\" from \"started but errored mid-stream.\"\n *\n * @public\n */\nexport interface IAiStreamError {\n readonly type: 'error';\n readonly message: string;\n}\n\n/**\n * Discriminated union of events emitted by a streaming completion.\n * @public\n */\nexport type IAiStreamEvent = IAiStreamTextDelta | IAiStreamToolEvent | IAiStreamDone | IAiStreamError;\n\n/**\n * Describes a single AI provider — single source of truth for all metadata.\n * @public\n */\nexport interface IAiProviderDescriptor {\n /** Provider identifier (e.g. 'xai-grok', 'anthropic') */\n readonly id: AiProviderId;\n /** Human-readable label (e.g. \"xAI Grok\") */\n readonly label: string;\n /** Button label for action buttons (e.g. \"AI Assist | Grok\") */\n readonly buttonLabel: string;\n /** Whether this provider requires an API key secret */\n readonly needsSecret: boolean;\n /** Which API adapter format to use */\n readonly apiFormat: AiApiFormat;\n /** Base URL for the API (e.g. 'https://api.x.ai/v1') */\n readonly baseUrl: string;\n /** Default model specification — string or context-aware map. */\n readonly defaultModel: ModelSpec;\n /** Which server-side tools this provider supports (empty = none). */\n readonly supportedTools: ReadonlyArray<AiServerToolType>;\n /** Whether this provider's API enforces CORS restrictions that prevent direct browser calls. */\n readonly corsRestricted: boolean;\n /**\n * Whether this provider's streaming completion endpoint requires a proxy\n * for direct browser calls. Some providers gate streaming separately from\n * non-streaming (rare), so this is tracked independently from\n * {@link IAiProviderDescriptor.corsRestricted}.\n *\n * @remarks\n * When `true`, `callProviderCompletionStream` rejects up front unless the\n * call is being routed through a proxy.\n */\n readonly streamingCorsRestricted: boolean;\n /**\n * Whether this provider's chat completions API accepts image input\n * (i.e. supports vision prompts). When false, calls with\n * `prompt.attachments` are rejected up front.\n */\n readonly acceptsImageInput: boolean;\n /**\n * Image-generation capabilities, scoped to model id prefixes. Empty or\n * undefined means the provider does not support image generation.\n *\n * @remarks\n * The dispatcher matches the resolved model id against each rule's\n * `modelPrefix` and selects the longest match (see\n * {@link AiAssist.resolveImageCapability}). An empty `modelPrefix` is the\n * catch-all and matches every model id.\n *\n * Multiple entries support providers that host more than one image-API\n * surface under one baseUrl. Google Gemini is the canonical case: the\n * `imagen-*` family is predict-only via `:predict`, while\n * `gemini-2.5-flash-image` uses chat-style `:generateContent` and accepts\n * reference images. Listing both lets callers pick the right model and the\n * dispatcher routes accordingly.\n *\n * Image-model selection reuses the existing `image` {@link ModelSpecKey}.\n * Providers that declare `imageGeneration` should declare a model in\n * `defaultModel.image`, e.g. `{ base: 'gpt-4o', image: 'dall-e-3' }`.\n */\n readonly imageGeneration?: ReadonlyArray<IAiImageModelCapability>;\n}\n\n/**\n * Image-generation capability for a model family within a provider. Used as\n * an entry in {@link IAiProviderDescriptor.imageGeneration}.\n *\n * @public\n */\nexport interface IAiImageModelCapability {\n /**\n * Prefix matched against the resolved image model id. The empty string is\n * the catch-all and matches every model. When multiple rules' prefixes\n * match a model id, the longest prefix wins; ties are broken by\n * first-encountered.\n */\n readonly modelPrefix: string;\n /** API format used to dispatch requests for matching models. */\n readonly format: AiImageApiFormat;\n /**\n * Whether matching models accept reference images via\n * {@link AiAssist.IAiImageGenerationParams.referenceImages}. When false or\n * undefined, calls that include reference images are rejected up front.\n *\n * @remarks\n * Per-model constraints beyond ref support (e.g. dall-e-3 ignores edits)\n * are not validated here and surface as provider 400s, consistent with the\n * existing image-generation policy.\n */\n readonly acceptsImageReferenceInput?: boolean;\n}\n\n// ============================================================================\n// Image Generation\n// ============================================================================\n\n/**\n * Options for image generation requests.\n *\n * @remarks\n * Provider compatibility is documented per field. The library does not\n * pre-validate against per-model constraints (e.g. `dall-e-3` rejects\n * `count > 1`); provider 400 errors surface through the failure path.\n *\n * @public\n */\nexport interface IAiImageGenerationOptions {\n /**\n * Image dimensions. Used by openai-format providers (mapped to the\n * provider's `size` field). Ignored by Imagen — use\n * {@link IAiImageGenerationOptions.imagen} `aspectRatio` instead.\n *\n * Note: each model has its own accepted set; `dall-e-3` only accepts the\n * values listed here.\n */\n readonly size?: '1024x1024' | '1024x1792' | '1792x1024' | 'auto';\n /**\n * Number of images to generate. Default 1.\n *\n * Note: `dall-e-3` rejects `count > 1`.\n */\n readonly count?: number;\n /** Generation quality hint where supported. */\n readonly quality?: 'standard' | 'high';\n /** Random seed for reproducibility, where supported. */\n readonly seed?: number;\n /**\n * Imagen-specific options. Ignored by other providers.\n */\n readonly imagen?: {\n readonly negativePrompt?: string;\n readonly aspectRatio?: '1:1' | '3:4' | '4:3' | '9:16' | '16:9';\n };\n}\n\n/**\n * Parameters for an image-generation request.\n * @public\n */\nexport interface IAiImageGenerationParams {\n /** The text prompt describing the desired image. */\n readonly prompt: string;\n /** Optional generation options. */\n readonly options?: IAiImageGenerationOptions;\n /**\n * Optional reference images. When present, the provider will use them as\n * visual context (e.g. to preserve a character's appearance across multiple\n * generations). The dispatcher resolves the\n * {@link AiAssist.IAiImageModelCapability} for the requested model and\n * rejects the call up front if `acceptsImageReferenceInput` is not set on\n * the matching capability. An empty array is treated identically to\n * `undefined`.\n */\n readonly referenceImages?: ReadonlyArray<IAiImageAttachment>;\n}\n\n/**\n * A single generated image.\n * @public\n */\nexport interface IAiGeneratedImage extends IAiImageData {\n /**\n * The prompt as rewritten by the provider, if any. OpenAI's image models\n * commonly rewrite prompts; other providers do not.\n */\n readonly revisedPrompt?: string;\n}\n\n// ============================================================================\n// Model Catalog (listModels)\n// ============================================================================\n\n/**\n * Capability vocabulary used to describe what a model can do. Used as both\n * a filter and as a tag in {@link AiAssist.IAiModelInfo.capabilities}.\n *\n * @remarks\n * Adding a new capability is cheap; adding the *first* one after consumers\n * already exist forces churn. The initial vocabulary is intentionally broad\n * even though only `image-generation` is fully exercised today.\n *\n * @public\n */\nexport type AiModelCapability = 'chat' | 'tools' | 'vision' | 'image-generation';\n\n/**\n * Information about a single model returned by a provider's list endpoint,\n * with capabilities already resolved (native + config rules).\n * @public\n */\nexport interface IAiModelInfo {\n /** Provider-native model identifier. */\n readonly id: string;\n /** Resolved capability set — union of native declarations and config rules. */\n readonly capabilities: ReadonlySet<AiModelCapability>;\n /** Friendly name for display, when known. */\n readonly displayName?: string;\n}\n\n/**\n * One rule in an {@link IAiModelCapabilityConfig}. Multiple rules can match\n * a single model — their capability arrays are unioned.\n * @public\n */\nexport interface IAiModelCapabilityRule {\n /** RegExp tested against the model id (using `.test`). */\n readonly idPattern: RegExp;\n /** Capabilities this rule attributes to matching models. */\n readonly capabilities: ReadonlyArray<AiModelCapability>;\n /**\n * Friendly display-name override for matching models. The function form\n * lets one rule format many ids (e.g. `(id) => id.toUpperCase()`).\n * If multiple matching rules supply `displayName`, the first match wins.\n */\n readonly displayName?: string | ((id: string) => string);\n}\n\n/**\n * Configuration that maps model id patterns to capabilities. Used to\n * augment (or, where the provider supplies no capability info, fully\n * derive) the capability set for each listed model.\n * @public\n */\nexport interface IAiModelCapabilityConfig {\n /** Per-provider rules. Tried before {@link AiAssist.IAiModelCapabilityConfig.global}. */\n readonly perProvider?: { readonly [P in AiProviderId]?: ReadonlyArray<IAiModelCapabilityRule> };\n /** Cross-provider fallback rules. */\n readonly global?: ReadonlyArray<IAiModelCapabilityRule>;\n}\n\n/**\n * Result of an image-generation call.\n * @public\n */\nexport interface IAiImageGenerationResponse {\n /** The generated images, in provider-returned order. */\n readonly images: ReadonlyArray<IAiGeneratedImage>;\n}\n\n// ============================================================================\n// Settings\n// ============================================================================\n\n/**\n * Configuration for a single AI assist provider.\n * @public\n */\nexport interface IAiAssistProviderConfig {\n /** Which provider this configures */\n readonly provider: AiProviderId;\n /** For API-based providers: the keystore secret name holding the API key */\n readonly secretName?: string;\n /** Optional model override — string or context-aware map. */\n readonly model?: ModelSpec;\n /** Tool enablement/configuration. Tools are disabled unless explicitly enabled. */\n readonly tools?: ReadonlyArray<IAiToolEnablement>;\n}\n\n/**\n * AI assist settings — which providers are enabled and their configuration.\n * @public\n */\nexport interface IAiAssistSettings {\n /** Enabled providers and their configuration. */\n readonly providers: ReadonlyArray<IAiAssistProviderConfig>;\n /** Which enabled provider is the default for the main button. Falls back to first in list. */\n readonly defaultProvider?: AiProviderId;\n /** Optional proxy URL for routing API requests through a backend server (e.g. `http://localhost:3002`). */\n readonly proxyUrl?: string;\n /** When true, route all providers through the proxy. When false (default), only CORS-restricted providers use the proxy. */\n readonly proxyAllProviders?: boolean;\n}\n\n/**\n * Default AI assist settings (copy-paste only).\n * @public\n */\nexport const DEFAULT_AI_ASSIST: IAiAssistSettings = {\n providers: [{ provider: 'copy-paste' }]\n};\n\n// ============================================================================\n// Keystore Interface\n// ============================================================================\n\n/**\n * Minimal keystore interface for AI assist API key resolution.\n * Satisfied structurally by the concrete `KeyStore` class from `@fgv/ts-extras`.\n * @public\n */\nexport interface IAiAssistKeyStore {\n /** Whether the keystore is currently unlocked */\n readonly isUnlocked: boolean;\n /** Check if a named secret exists */\n hasSecret(name: string): Result<boolean>;\n /** Get an API key by secret name */\n getApiKey(name: string): Result<string>;\n}\n"]}
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/model.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,wEAAwE;AACxE,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,kDAAkD;AAClD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,yEAAyE;AACzE,gFAAgF;AAChF,gFAAgF;AAChF,YAAY;AA8BZ;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAmB;IAC3C,OAAO,QAAQ,KAAK,CAAC,QAAQ,WAAW,KAAK,CAAC,MAAM,EAAE,CAAC;AACzD,CAAC;AAuBD,+EAA+E;AAC/E,WAAW;AACX,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,OAAO,QAAQ;IAYnB,YAAmB,IAAY,EAAE,MAAc,EAAE,WAA+C;QAC9F,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,aAAX,WAAW,cAAX,WAAW,GAAI,EAAE,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,IAAW,QAAQ;QACjB,MAAM,QAAQ,GACZ,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC;YACzB,CAAC,CAAC,QAAQ,IAAI,CAAC,WAAW,CAAC,MAAM,qDAAqD;YACtF,CAAC,CAAC,EAAE,CAAC;QACT,OAAO,GAAG,IAAI,CAAC,IAAI,GAAG,QAAQ,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;IACrD,CAAC;CACF;AA6ED;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAgC,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAExF;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAiB,MAAM,CAAC;AAiCxD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,YAAY,CAAC,IAAe,EAAE,OAAgB;IAC5D,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sCAAsC;IACtC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QAC7C,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,sBAAsB;IACtB,IAAI,mBAAmB,IAAI,IAAI,EAAE,CAAC;QAChC,OAAO,YAAY,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,yCAAyC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,6FAA6F;IAC7F,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AA2ZD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;CACxC,CAAC","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * Core types for AI assist: prompt class, provider descriptors, settings, and chat messages.\n * @packageDocumentation\n */\n\nimport { type Result } from '@fgv/ts-utils';\n\n// ============================================================================\n// Image Data\n// ============================================================================\n\n/**\n * Universal image representation used for both image input (vision prompts)\n * and image output (generation responses).\n *\n * @remarks\n * The base64 string is raw — no `data:` URL prefix. Use {@link AiAssist.toDataUrl} to\n * format it for browser-display contexts.\n *\n * @public\n */\nexport interface IAiImageData {\n /** MIME type, e.g. `'image/png'`, `'image/jpeg'`, `'image/webp'`. */\n readonly mimeType: string;\n /** Base64-encoded image bytes (no `data:` prefix). */\n readonly base64: string;\n}\n\n/**\n * Formats an {@link IAiImageData} as a `data:` URL suitable for browser display.\n * @param image - The image to format\n * @returns A `data:<mime>;base64,<data>` URL string\n * @public\n */\nexport function toDataUrl(image: IAiImageData): string {\n return `data:${image.mimeType};base64,${image.base64}`;\n}\n\n/**\n * Image attachment for a vision (image-input) prompt.\n *\n * @remarks\n * Extends {@link IAiImageData} with an OpenAI-specific `detail` hint that is\n * silently ignored by Anthropic, Gemini, and other providers.\n *\n * @public\n */\nexport interface IAiImageAttachment extends IAiImageData {\n /**\n * OpenAI vision detail hint:\n * - `'low'`: faster, cheaper, lower fidelity\n * - `'high'`: slower, more expensive, higher fidelity\n * - `'auto'` (default): provider chooses\n *\n * Ignored by providers other than OpenAI.\n */\n readonly detail?: 'low' | 'high' | 'auto';\n}\n\n// ============================================================================\n// AiPrompt\n// ============================================================================\n\n/**\n * A structured AI prompt with system/user split for direct API calls,\n * and a lazily-constructed combined version for copy/paste workflows.\n * @public\n */\nexport class AiPrompt {\n /** System instructions: schema documentation, format rules, general guidance. */\n public readonly system: string;\n /** User request: the specific entity generation request. */\n public readonly user: string;\n /**\n * Optional image attachments. When present, vision-capable providers will\n * include them in the user message; non-vision providers will reject the\n * call up front (see {@link AiAssist.IAiProviderDescriptor.acceptsImageInput}).\n */\n public readonly attachments: ReadonlyArray<IAiImageAttachment>;\n\n public constructor(user: string, system: string, attachments?: ReadonlyArray<IAiImageAttachment>) {\n this.system = system;\n this.user = user;\n this.attachments = attachments ?? [];\n }\n\n /**\n * Combined single-string version (user + system joined) for copy/paste.\n * When attachments are present, includes a sentinel noting they aren't\n * part of the copied text.\n */\n public get combined(): string {\n const sentinel =\n this.attachments.length > 0\n ? `\\n\\n[${this.attachments.length} image attachment(s) — not included in copied text]`\n : '';\n return `${this.user}${sentinel}\\n\\n${this.system}`;\n }\n}\n\n// ============================================================================\n// Chat Message\n// ============================================================================\n\n/**\n * A single chat message in OpenAI format.\n * @public\n */\nexport interface IChatMessage {\n /** Message role */\n readonly role: 'system' | 'user' | 'assistant';\n /** Message content */\n readonly content: string;\n}\n\n// ============================================================================\n// Server-Side Tools\n// ============================================================================\n\n/**\n * Built-in server-side tool types supported across providers.\n * @public\n */\nexport type AiServerToolType = 'web_search';\n\n/**\n * Configuration specific to web search tools.\n * @public\n */\nexport interface IAiWebSearchToolConfig {\n readonly type: 'web_search';\n /** Optional: restrict search to these domains. */\n readonly allowedDomains?: ReadonlyArray<string>;\n /** Optional: exclude these domains from search. */\n readonly blockedDomains?: ReadonlyArray<string>;\n /** Optional: max number of searches per request. */\n readonly maxUses?: number;\n /**\n * Optional: enable image understanding during web search.\n * When true, the model can view and analyze images found during search.\n * Currently supported by xAI only; ignored by other providers.\n */\n readonly enableImageUnderstanding?: boolean;\n}\n\n/**\n * Union of all server-side tool configurations. Discriminated on `type`.\n * @public\n */\nexport type AiServerToolConfig = IAiWebSearchToolConfig;\n\n/**\n * Declares a tool as enabled/disabled in provider settings.\n * Tools are disabled by default — consuming apps must opt in explicitly.\n * @public\n */\nexport interface IAiToolEnablement {\n /** Which tool type. */\n readonly type: AiServerToolType;\n /** Whether this tool is enabled by default for this provider. */\n readonly enabled: boolean;\n /** Optional tool-specific configuration. */\n readonly config?: AiServerToolConfig;\n}\n\n// ============================================================================\n// Model Specification\n// ============================================================================\n\n/**\n * Known context keys for model specification maps.\n * @public\n */\nexport type ModelSpecKey = 'base' | 'tools' | 'image';\n\n/**\n * All valid {@link ModelSpecKey} values.\n * @public\n */\nexport const allModelSpecKeys: ReadonlyArray<ModelSpecKey> = ['base', 'tools', 'image'];\n\n/**\n * Default context key used as fallback when resolving a {@link ModelSpec}.\n * @public\n */\nexport const MODEL_SPEC_BASE_KEY: ModelSpecKey = 'base';\n\n/**\n * A model specification: either a simple model string or a record mapping\n * context keys to nested model specs.\n *\n * @remarks\n * A bare string is equivalent to `{ base: string }`. This keeps the simple\n * case simple while allowing context-aware model selection (e.g. different\n * models for tool-augmented vs. base completions).\n *\n * @example\n * ```typescript\n * // Simple — same model for all contexts:\n * const simple: ModelSpec = 'grok-4-1-fast';\n *\n * // Context-aware — reasoning model when tools are active:\n * const split: ModelSpec = { base: 'grok-4-1-fast', tools: 'grok-4-1-fast-reasoning' };\n *\n * // Future nested — per-tool model selection:\n * const nested: ModelSpec = { base: 'grok-fast', tools: { base: 'grok-r', image: 'grok-v' } };\n * ```\n * @public\n */\nexport interface IModelSpecMap {\n readonly [key: string]: ModelSpec;\n}\n\n/**\n * @public\n */\nexport type ModelSpec = string | IModelSpecMap;\n\n/**\n * Resolves a {@link ModelSpec} to a concrete model string given an optional context key.\n *\n * @remarks\n * Resolution rules:\n * 1. If the spec is a string, return it directly (context is irrelevant).\n * 2. If the spec is an object and the context key exists, recurse into that branch.\n * 3. Otherwise, fall back to the {@link MODEL_SPEC_BASE_KEY | 'base'} key.\n * 4. If neither context nor `'base'` exists, use the first available value.\n *\n * @param spec - The model specification to resolve\n * @param context - Optional context key (e.g. `'tools'`)\n * @returns The resolved model string\n * @public\n */\nexport function resolveModel(spec: ModelSpec, context?: string): string {\n if (typeof spec === 'string') {\n return spec;\n }\n\n // Try the requested context key first\n if (context !== undefined && context in spec) {\n return resolveModel(spec[context]);\n }\n\n // Fall back to 'base'\n if (MODEL_SPEC_BASE_KEY in spec) {\n return resolveModel(spec[MODEL_SPEC_BASE_KEY]);\n }\n\n // Last resort: first value in the record\n const first = Object.values(spec)[0];\n /* c8 ignore next 3 - defensive: only reachable with empty object (prevented by converter) */\n if (first === undefined) {\n return '';\n }\n return resolveModel(first);\n}\n\n// ============================================================================\n// Provider Descriptor\n// ============================================================================\n\n/**\n * All known AI provider identifiers.\n * @public\n */\nexport type AiProviderId =\n | 'copy-paste'\n | 'xai-grok'\n | 'openai'\n | 'openai-compat'\n | 'anthropic'\n | 'google-gemini'\n | 'groq'\n | 'mistral'\n | 'ollama';\n\n/**\n * API format categories for provider routing.\n * @public\n */\nexport type AiApiFormat = 'openai' | 'anthropic' | 'gemini';\n\n/**\n * API format categories for image-generation provider routing.\n *\n * @remarks\n * - `'openai-images'` — OpenAI Images API. Routes to `/images/generations`\n * (text-only) or `/images/edits` (when reference images are present).\n * - `'xai-images'` — xAI Images API. Same wire shape as OpenAI but text-only;\n * no reference-image support on grok-2-image.\n * - `'gemini-imagen'` — Google Imagen `:predict` endpoint. Text-only.\n * - `'gemini-image-out'` — Google Gemini chat-style `:generateContent`\n * endpoint that returns image parts (Gemini 2.5 Flash Image / \"Nano\n * Banana\"). Accepts reference images.\n *\n * @public\n */\nexport type AiImageApiFormat = 'openai-images' | 'gemini-imagen' | 'xai-images' | 'gemini-image-out';\n\n// ============================================================================\n// Completion Response\n// ============================================================================\n\n/**\n * Result of an AI provider completion call.\n * @public\n */\nexport interface IAiCompletionResponse {\n /** The generated text content */\n readonly content: string;\n /** Whether the response was truncated due to token limits */\n readonly truncated: boolean;\n}\n\n// ============================================================================\n// Streaming Events\n// ============================================================================\n\n/**\n * A text-content delta arriving during a streaming completion.\n * @public\n */\nexport interface IAiStreamTextDelta {\n readonly type: 'text-delta';\n /** The newly arrived text fragment. */\n readonly delta: string;\n}\n\n/**\n * A server-side tool progress event arriving during a streaming completion.\n * Surfaced for providers that emit explicit tool-progress markers (OpenAI\n * Responses API, Anthropic). Gemini's grounding doesn't emit these.\n * @public\n */\nexport interface IAiStreamToolEvent {\n readonly type: 'tool-event';\n /** Which server-side tool this event describes. */\n readonly toolType: AiServerToolType;\n /** Tool lifecycle phase. */\n readonly phase: 'started' | 'completed';\n /**\n * Optional provider-specific detail. For web_search this is typically the\n * search query when available; format varies by provider.\n */\n readonly detail?: string;\n}\n\n/**\n * Terminal success event for a streaming completion. Carries the aggregated\n * full text and truncation status for callers that want both the progressive\n * UI and the complete result.\n * @public\n */\nexport interface IAiStreamDone {\n readonly type: 'done';\n /** Whether the response was truncated due to token limits. */\n readonly truncated: boolean;\n /** The full concatenated text from all `text-delta` events. */\n readonly fullText: string;\n}\n\n/**\n * Terminal failure event for a streaming completion. After this event no\n * further events are emitted.\n *\n * @remarks\n * Connection-time failures (auth, network, pre-flight CORS rejection) are\n * surfaced via the outer `Result.fail` returned by\n * `callProviderCompletionStream` rather than as an `error` event, so callers\n * can distinguish \"didn't start\" from \"started but errored mid-stream.\"\n *\n * @public\n */\nexport interface IAiStreamError {\n readonly type: 'error';\n readonly message: string;\n}\n\n/**\n * Discriminated union of events emitted by a streaming completion.\n * @public\n */\nexport type IAiStreamEvent = IAiStreamTextDelta | IAiStreamToolEvent | IAiStreamDone | IAiStreamError;\n\n/**\n * Describes a single AI provider — single source of truth for all metadata.\n * @public\n */\nexport interface IAiProviderDescriptor {\n /** Provider identifier (e.g. 'xai-grok', 'anthropic') */\n readonly id: AiProviderId;\n /** Human-readable label (e.g. \"xAI Grok\") */\n readonly label: string;\n /** Button label for action buttons (e.g. \"AI Assist | Grok\") */\n readonly buttonLabel: string;\n /** Whether this provider requires an API key secret */\n readonly needsSecret: boolean;\n /** Which API adapter format to use */\n readonly apiFormat: AiApiFormat;\n /** Base URL for the API (e.g. 'https://api.x.ai/v1') */\n readonly baseUrl: string;\n /** Default model specification — string or context-aware map. */\n readonly defaultModel: ModelSpec;\n /** Which server-side tools this provider supports (empty = none). */\n readonly supportedTools: ReadonlyArray<AiServerToolType>;\n /** Whether this provider's API enforces CORS restrictions that prevent direct browser calls. */\n readonly corsRestricted: boolean;\n /**\n * Whether this provider's streaming completion endpoint requires a proxy\n * for direct browser calls. Some providers gate streaming separately from\n * non-streaming (rare), so this is tracked independently from\n * {@link IAiProviderDescriptor.corsRestricted}.\n *\n * @remarks\n * When `true`, `callProviderCompletionStream` rejects up front unless the\n * call is being routed through a proxy.\n */\n readonly streamingCorsRestricted: boolean;\n /**\n * Whether this provider's chat completions API accepts image input\n * (i.e. supports vision prompts). When false, calls with\n * `prompt.attachments` are rejected up front.\n */\n readonly acceptsImageInput: boolean;\n /**\n * Image-generation capabilities, scoped to model id prefixes. Empty or\n * undefined means the provider does not support image generation.\n *\n * @remarks\n * The dispatcher matches the resolved model id against each rule's\n * `modelPrefix` and selects the longest match (see\n * {@link AiAssist.resolveImageCapability}). An empty `modelPrefix` is the\n * catch-all and matches every model id.\n *\n * Multiple entries support providers that host more than one image-API\n * surface under one baseUrl. Google Gemini is the canonical case: the\n * `imagen-*` family is predict-only via `:predict`, while\n * `gemini-2.5-flash-image` uses chat-style `:generateContent` and accepts\n * reference images. Listing both lets callers pick the right model and the\n * dispatcher routes accordingly.\n *\n * Image-model selection reuses the existing `image` {@link ModelSpecKey}.\n * Providers that declare `imageGeneration` should declare a model in\n * `defaultModel.image`, e.g. `{ base: 'gpt-4o', image: 'dall-e-3' }`.\n */\n readonly imageGeneration?: ReadonlyArray<IAiImageModelCapability>;\n}\n\n/**\n * Image-generation capability for a model family within a provider. Used as\n * an entry in {@link IAiProviderDescriptor.imageGeneration}.\n *\n * @public\n */\nexport interface IAiImageModelCapability {\n /**\n * Prefix matched against the resolved image model id. The empty string is\n * the catch-all and matches every model. When multiple rules' prefixes\n * match a model id, the longest prefix wins; ties are broken by\n * first-encountered.\n */\n readonly modelPrefix: string;\n /** API format used to dispatch requests for matching models. */\n readonly format: AiImageApiFormat;\n /**\n * Whether matching models accept reference images via\n * {@link AiAssist.IAiImageGenerationParams.referenceImages}. When false or\n * undefined, calls that include reference images are rejected up front.\n *\n * @remarks\n * Per-model constraints beyond ref support (e.g. dall-e-3 ignores edits)\n * are not validated here and surface as provider 400s, consistent with the\n * existing image-generation policy.\n */\n readonly acceptsImageReferenceInput?: boolean;\n}\n\n// ============================================================================\n// Image Generation\n// ============================================================================\n\n/**\n * Options for image generation requests.\n *\n * @remarks\n * Provider compatibility is documented per field. The library does not\n * pre-validate against per-model constraints (e.g. `dall-e-3` rejects\n * `count > 1`); provider 400 errors surface through the failure path.\n *\n * @public\n */\nexport interface IAiImageGenerationOptions {\n /**\n * Image dimensions. Used by openai-format providers (mapped to the\n * provider's `size` field). Ignored by Imagen — use\n * {@link IAiImageGenerationOptions.imagen} `aspectRatio` instead.\n *\n * Note: each model has its own accepted set; `dall-e-3` only accepts the\n * values listed here.\n */\n readonly size?: '1024x1024' | '1024x1792' | '1792x1024' | 'auto';\n /**\n * Number of images to generate. Default 1.\n *\n * Note: `dall-e-3` rejects `count > 1`.\n */\n readonly count?: number;\n /** Generation quality hint where supported. */\n readonly quality?: 'standard' | 'high';\n /** Random seed for reproducibility, where supported. */\n readonly seed?: number;\n /**\n * Imagen-specific options. Ignored by other providers.\n */\n readonly imagen?: {\n readonly negativePrompt?: string;\n readonly aspectRatio?: '1:1' | '3:4' | '4:3' | '9:16' | '16:9';\n };\n}\n\n/**\n * Parameters for an image-generation request.\n * @public\n */\nexport interface IAiImageGenerationParams {\n /** The text prompt describing the desired image. */\n readonly prompt: string;\n /** Optional generation options. */\n readonly options?: IAiImageGenerationOptions;\n /**\n * Optional reference images. When present, the provider will use them as\n * visual context (e.g. to preserve a character's appearance across multiple\n * generations). The dispatcher resolves the\n * {@link AiAssist.IAiImageModelCapability} for the requested model and\n * rejects the call up front if `acceptsImageReferenceInput` is not set on\n * the matching capability. An empty array is treated identically to\n * `undefined`.\n */\n readonly referenceImages?: ReadonlyArray<IAiImageAttachment>;\n}\n\n/**\n * A single generated image.\n * @public\n */\nexport interface IAiGeneratedImage extends IAiImageData {\n /**\n * The prompt as rewritten by the provider, if any. OpenAI's image models\n * commonly rewrite prompts; other providers do not.\n */\n readonly revisedPrompt?: string;\n}\n\n// ============================================================================\n// Model Catalog (listModels)\n// ============================================================================\n\n/**\n * Capability vocabulary used to describe what a model can do. Used as both\n * a filter and as a tag in {@link AiAssist.IAiModelInfo.capabilities}.\n *\n * @remarks\n * Adding a new capability is cheap; adding the *first* one after consumers\n * already exist forces churn. The initial vocabulary is intentionally broad\n * even though only `image-generation` is fully exercised today.\n *\n * @public\n */\nexport type AiModelCapability = 'chat' | 'tools' | 'vision' | 'image-generation';\n\n/**\n * Information about a single model returned by a provider's list endpoint,\n * with capabilities already resolved (native + config rules).\n * @public\n */\nexport interface IAiModelInfo {\n /** Provider-native model identifier. */\n readonly id: string;\n /** Resolved capability set — union of native declarations and config rules. */\n readonly capabilities: ReadonlySet<AiModelCapability>;\n /** Friendly name for display, when known. */\n readonly displayName?: string;\n}\n\n/**\n * One rule in an {@link IAiModelCapabilityConfig}. Multiple rules can match\n * a single model — their capability arrays are unioned.\n * @public\n */\nexport interface IAiModelCapabilityRule {\n /** RegExp tested against the model id (using `.test`). */\n readonly idPattern: RegExp;\n /** Capabilities this rule attributes to matching models. */\n readonly capabilities: ReadonlyArray<AiModelCapability>;\n /**\n * Friendly display-name override for matching models. The function form\n * lets one rule format many ids (e.g. `(id) => id.toUpperCase()`).\n * If multiple matching rules supply `displayName`, the first match wins.\n */\n readonly displayName?: string | ((id: string) => string);\n}\n\n/**\n * Configuration that maps model id patterns to capabilities. Used to\n * augment (or, where the provider supplies no capability info, fully\n * derive) the capability set for each listed model.\n * @public\n */\nexport interface IAiModelCapabilityConfig {\n /** Per-provider rules. Tried before {@link AiAssist.IAiModelCapabilityConfig.global}. */\n readonly perProvider?: { readonly [P in AiProviderId]?: ReadonlyArray<IAiModelCapabilityRule> };\n /** Cross-provider fallback rules. */\n readonly global?: ReadonlyArray<IAiModelCapabilityRule>;\n}\n\n/**\n * Result of an image-generation call.\n * @public\n */\nexport interface IAiImageGenerationResponse {\n /** The generated images, in provider-returned order. */\n readonly images: ReadonlyArray<IAiGeneratedImage>;\n}\n\n// ============================================================================\n// Settings\n// ============================================================================\n\n/**\n * Configuration for a single AI assist provider.\n * @public\n */\nexport interface IAiAssistProviderConfig {\n /** Which provider this configures */\n readonly provider: AiProviderId;\n /** For API-based providers: the keystore secret name holding the API key */\n readonly secretName?: string;\n /** Optional model override — string or context-aware map. */\n readonly model?: ModelSpec;\n /** Tool enablement/configuration. Tools are disabled unless explicitly enabled. */\n readonly tools?: ReadonlyArray<IAiToolEnablement>;\n /**\n * Optional caller-supplied endpoint URL (http/https). Overrides\n * `descriptor.baseUrl` for this provider. Used to point a provider at a\n * self-hosted server (Ollama, LM Studio, llama.cpp's openai-server) or a\n * local proxy. Validation lives in `@fgv/ts-extras` — query strings,\n * fragments, and userinfo are rejected.\n */\n readonly endpoint?: string;\n}\n\n/**\n * AI assist settings — which providers are enabled and their configuration.\n * @public\n */\nexport interface IAiAssistSettings {\n /** Enabled providers and their configuration. */\n readonly providers: ReadonlyArray<IAiAssistProviderConfig>;\n /** Which enabled provider is the default for the main button. Falls back to first in list. */\n readonly defaultProvider?: AiProviderId;\n /** Optional proxy URL for routing API requests through a backend server (e.g. `http://localhost:3002`). */\n readonly proxyUrl?: string;\n /** When true, route all providers through the proxy. When false (default), only CORS-restricted providers use the proxy. */\n readonly proxyAllProviders?: boolean;\n}\n\n/**\n * Default AI assist settings (copy-paste only).\n * @public\n */\nexport const DEFAULT_AI_ASSIST: IAiAssistSettings = {\n providers: [{ provider: 'copy-paste' }]\n};\n\n// ============================================================================\n// Keystore Interface\n// ============================================================================\n\n/**\n * Minimal keystore interface for AI assist API key resolution.\n * Satisfied structurally by the concrete `KeyStore` class from `@fgv/ts-extras`.\n * @public\n */\nexport interface IAiAssistKeyStore {\n /** Whether the keystore is currently unlocked */\n readonly isUnlocked: boolean;\n /** Check if a named secret exists */\n hasSecret(name: string): Result<boolean>;\n /** Get an API key by secret name */\n getApiKey(name: string): Result<string>;\n}\n"]}
@@ -101,6 +101,19 @@ const BUILTIN_PROVIDERS = [
101
101
  streamingCorsRestricted: false,
102
102
  acceptsImageInput: false
103
103
  },
104
+ {
105
+ id: 'ollama',
106
+ label: 'Ollama (self-hosted)',
107
+ buttonLabel: 'AI Assist | Ollama',
108
+ needsSecret: false,
109
+ apiFormat: 'openai',
110
+ baseUrl: 'http://localhost:11434/v1',
111
+ defaultModel: '',
112
+ supportedTools: [],
113
+ corsRestricted: false,
114
+ streamingCorsRestricted: false,
115
+ acceptsImageInput: false
116
+ },
104
117
  {
105
118
  id: 'openai',
106
119
  label: 'OpenAI',
@@ -122,6 +135,19 @@ const BUILTIN_PROVIDERS = [
122
135
  { modelPrefix: '', format: 'openai-images' }
123
136
  ]
124
137
  },
138
+ {
139
+ id: 'openai-compat',
140
+ label: 'OpenAI-compatible (self-hosted)',
141
+ buttonLabel: 'AI Assist | OpenAI-compat',
142
+ needsSecret: false,
143
+ apiFormat: 'openai',
144
+ baseUrl: '',
145
+ defaultModel: '',
146
+ supportedTools: [],
147
+ corsRestricted: false,
148
+ streamingCorsRestricted: false,
149
+ acceptsImageInput: false
150
+ },
125
151
  {
126
152
  id: 'xai-grok',
127
153
  label: 'xAI Grok',