@umituz/web-ai 1.1.0 → 1.2.0

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 (48) hide show
  1. package/README.md +29 -3
  2. package/dist/{cancellation-error-_39nYWwz.d.cts → cancellation-error-BWb59Wkj.d.cts} +1 -1
  3. package/dist/{cancellation-error-hYcE9AS9.d.ts → cancellation-error-D9LETLCD.d.ts} +1 -1
  4. package/dist/chat/index.cjs +1 -0
  5. package/dist/chat/index.cjs.map +1 -1
  6. package/dist/chat/index.js +1 -0
  7. package/dist/chat/index.js.map +1 -1
  8. package/dist/content/index.cjs +622 -8
  9. package/dist/content/index.cjs.map +1 -1
  10. package/dist/content/index.d.cts +2 -2
  11. package/dist/content/index.d.ts +2 -2
  12. package/dist/content/index.js +622 -8
  13. package/dist/content/index.js.map +1 -1
  14. package/dist/context-manager-CuZz0vnC.d.cts +104 -0
  15. package/dist/context-manager-CuZz0vnC.d.ts +104 -0
  16. package/dist/core/index.cjs +147 -10
  17. package/dist/core/index.cjs.map +1 -1
  18. package/dist/core/index.d.cts +91 -73
  19. package/dist/core/index.d.ts +91 -73
  20. package/dist/core/index.js +141 -11
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/{create-ai-D-oEWzf9.d.ts → create-ai-Cq3HkJxd.d.ts} +61 -1
  23. package/dist/{create-ai-DRysRImc.d.cts → create-ai-DDyTVzef.d.cts} +61 -1
  24. package/dist/hooks/index.cjs +747 -15
  25. package/dist/hooks/index.cjs.map +1 -1
  26. package/dist/hooks/index.d.cts +57 -4
  27. package/dist/hooks/index.d.ts +57 -4
  28. package/dist/hooks/index.js +746 -16
  29. package/dist/hooks/index.js.map +1 -1
  30. package/dist/index.cjs +1180 -242
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +7 -6
  33. package/dist/index.d.ts +7 -6
  34. package/dist/index.js +1140 -243
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers/index.cjs +1212 -362
  37. package/dist/providers/index.cjs.map +1 -1
  38. package/dist/providers/index.d.cts +422 -10
  39. package/dist/providers/index.d.ts +422 -10
  40. package/dist/providers/index.js +1179 -363
  41. package/dist/providers/index.js.map +1 -1
  42. package/dist/remote-chat-client-LrNlew4n.d.cts +70 -0
  43. package/dist/remote-chat-client-LrNlew4n.d.ts +70 -0
  44. package/dist/{requestQueue-D89wx91Q.d.cts → requestQueue--h2uZ-Q3.d.cts} +1 -1
  45. package/dist/{requestQueue-AP6n_bRH.d.ts → requestQueue-BGeCy4Fd.d.ts} +1 -1
  46. package/package.json +1 -1
  47. package/dist/tool-call-accumulator-C7KhaXZA.d.cts +0 -69
  48. package/dist/tool-call-accumulator-C7KhaXZA.d.ts +0 -69
@@ -205,6 +205,68 @@ async function extractErrorMessageFromResponse(response) {
205
205
  return buildHttpStatusFallback(response.status, response.statusText);
206
206
  }
207
207
 
208
+ // src/core/errors/unified-ai-error.ts
209
+ var STATUS_TO_CODE = {
210
+ 400: "invalid_request",
211
+ 401: "invalid_key",
212
+ 403: "invalid_key",
213
+ 404: "model_unavailable",
214
+ 413: "invalid_request",
215
+ 422: "invalid_request",
216
+ 429: "rate_limited",
217
+ 500: "upstream_error",
218
+ 502: "upstream_error",
219
+ 503: "capacity",
220
+ 504: "timeout"
221
+ };
222
+ var CODE_MESSAGES = {
223
+ invalid_key: "The provider rejected the configured API key.",
224
+ missing_key: "The provider is not configured with an API key.",
225
+ rate_limited: "The provider rate limit was reached. Try again shortly.",
226
+ capacity: "The provider is temporarily at capacity. Try again shortly.",
227
+ timeout: "The model took too long to respond.",
228
+ invalid_request: "The request was rejected by the provider.",
229
+ model_unavailable: "The requested model is not available.",
230
+ upstream_error: "The provider returned an unexpected error.",
231
+ network: "Could not reach the provider.",
232
+ cancelled: "Generation was cancelled."
233
+ };
234
+ var AiError = class extends Error {
235
+ code;
236
+ constructor(code, message) {
237
+ super(message ?? CODE_MESSAGES[code]);
238
+ this.name = "AiError";
239
+ this.code = code;
240
+ }
241
+ get userMessage() {
242
+ return this.message;
243
+ }
244
+ };
245
+ function isApiKeyRejection(provider) {
246
+ if (provider.code === "invalid_api_key") return true;
247
+ return provider.type === void 0;
248
+ }
249
+ function errorFromStatus(status, detail, provider) {
250
+ let code = STATUS_TO_CODE[status] ?? "upstream_error";
251
+ if (status === 403 && provider && !isApiKeyRejection(provider)) {
252
+ code = "model_unavailable";
253
+ }
254
+ return new AiError(code, detail ? `${CODE_MESSAGES[code]} (${detail})` : void 0);
255
+ }
256
+ function errorFromUnknown(err) {
257
+ if (err instanceof AiError) return err;
258
+ if (err instanceof Error) {
259
+ if (err.name === "TimeoutError" || err.name === "AbortError") {
260
+ return new AiError("timeout");
261
+ }
262
+ if (err.name === "TypeError") {
263
+ return new AiError("network");
264
+ }
265
+ return new AiError("upstream_error", err.message);
266
+ }
267
+ return new AiError("upstream_error");
268
+ }
269
+
208
270
  // src/providers/base/provider.types.ts
209
271
  function isTextProvider(provider) {
210
272
  return provider.capabilities.includes("text");
@@ -971,7 +1033,7 @@ var AnthropicProvider = class {
971
1033
  );
972
1034
  }
973
1035
  const structuredPrompt = new StructuredPromptBuilder(
974
- messages[lastUserIndex].content
1036
+ messages[lastUserIndex]?.content ?? ""
975
1037
  ).withSchema(request.schema).build();
976
1038
  messages[lastUserIndex] = { role: "user", content: structuredPrompt };
977
1039
  const wireRequest = new AnthropicRequestBuilder(model).withRequest({ ...request, messages }).withEffort(this.config.effort).build();
@@ -1051,7 +1113,8 @@ function mapAnthropicUsage(usage) {
1051
1113
  }
1052
1114
  function findLastIndex(items, predicate) {
1053
1115
  for (let i = items.length - 1; i >= 0; i--) {
1054
- if (predicate(items[i])) return i;
1116
+ const item = items[i];
1117
+ if (item !== void 0 && predicate(item)) return i;
1055
1118
  }
1056
1119
  return -1;
1057
1120
  }
@@ -1898,7 +1961,7 @@ var GroqProvider = class {
1898
1961
  );
1899
1962
  }
1900
1963
  const structuredPrompt = new StructuredPromptBuilder(
1901
- messages[lastUserIndex].content
1964
+ messages[lastUserIndex]?.content ?? ""
1902
1965
  ).withSchema(request.schema).build();
1903
1966
  messages[lastUserIndex] = { role: "user", content: structuredPrompt };
1904
1967
  const wireRequest = GroqChatRequestBuilder.for(model).withMessages(messages).withConfig(this.generationConfig(request, STRUCTURED_TEMPERATURE)).withJsonResponse(true).build();
@@ -1992,7 +2055,8 @@ function toGroqMessages(messages, systemPrompt) {
1992
2055
  }
1993
2056
  function findLastIndex2(items, predicate) {
1994
2057
  for (let i = items.length - 1; i >= 0; i--) {
1995
- if (predicate(items[i])) return i;
2058
+ const item = items[i];
2059
+ if (item !== void 0 && predicate(item)) return i;
1996
2060
  }
1997
2061
  return -1;
1998
2062
  }
@@ -2207,7 +2271,7 @@ var GeminiProvider = class extends BaseProvider {
2207
2271
  }
2208
2272
  messages[lastUserIndex] = {
2209
2273
  role: "user",
2210
- content: new StructuredPromptBuilder(messages[lastUserIndex].content).withSchema(request.schema).build()
2274
+ content: new StructuredPromptBuilder(messages[lastUserIndex]?.content ?? "").withSchema(request.schema).build()
2211
2275
  };
2212
2276
  const result = await this.generate({ ...request, messages, temperature: 0.1 });
2213
2277
  return { value: this.jsonParser.parse(result.text), result };
@@ -2313,7 +2377,8 @@ function mapGeminiFinishReason(reason) {
2313
2377
  }
2314
2378
  function findLastIndex3(items, predicate) {
2315
2379
  for (let i = items.length - 1; i >= 0; i--) {
2316
- if (predicate(items[i])) return i;
2380
+ const item = items[i];
2381
+ if (item !== void 0 && predicate(item)) return i;
2317
2382
  }
2318
2383
  return -1;
2319
2384
  }
@@ -3519,6 +3584,546 @@ var PrunaProvider = class {
3519
3584
  }
3520
3585
  };
3521
3586
 
3587
+ // src/core/sse/tool-call-accumulator.ts
3588
+ var ToolCallAccumulator = class {
3589
+ slots = /* @__PURE__ */ new Map();
3590
+ push(delta) {
3591
+ const index = delta.index ?? 0;
3592
+ const slot = this.slots.get(index) ?? { id: "", name: "", args: "" };
3593
+ if (delta.id) slot.id = delta.id;
3594
+ if (delta.function?.name) slot.name = delta.function.name;
3595
+ if (delta.function?.arguments) slot.args += delta.function.arguments;
3596
+ this.slots.set(index, slot);
3597
+ }
3598
+ finish() {
3599
+ return Array.from(this.slots.entries()).sort(([a], [b]) => a - b).map(([, slot]) => ({
3600
+ id: slot.id,
3601
+ name: slot.name,
3602
+ arguments: slot.args
3603
+ })).filter((call) => call.id.length > 0 || call.name.length > 0);
3604
+ }
3605
+ };
3606
+
3607
+ // src/providers/adapters/openai-compatible.adapter.ts
3608
+ function toWireMessage(message) {
3609
+ if (message.role === "tool") {
3610
+ return { role: "tool", content: message.content, tool_call_id: message.toolCallId ?? "" };
3611
+ }
3612
+ if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
3613
+ return {
3614
+ role: "assistant",
3615
+ content: message.content,
3616
+ tool_calls: message.toolCalls.map((call) => ({
3617
+ id: call.id,
3618
+ type: "function",
3619
+ function: { name: call.name, arguments: call.arguments }
3620
+ }))
3621
+ };
3622
+ }
3623
+ if (message.role === "user") return { role: "user", content: message.content };
3624
+ return { role: message.role, content: message.content };
3625
+ }
3626
+ function mergeImages(messages, images) {
3627
+ if (images.length === 0) return [...messages];
3628
+ const parts = images.map((image) => ({
3629
+ type: "image_url",
3630
+ image_url: { url: `data:${image.mimeType};base64,${image.dataBase64}` }
3631
+ }));
3632
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3633
+ const message = messages[index];
3634
+ if (!message || message.role !== "user") continue;
3635
+ const textParts = typeof message.content === "string" ? message.content.length > 0 ? [{ type: "text", text: message.content }] : [] : message.content;
3636
+ return [
3637
+ ...messages.slice(0, index),
3638
+ { role: "user", content: [...textParts, ...parts] },
3639
+ ...messages.slice(index + 1)
3640
+ ];
3641
+ }
3642
+ return [...messages, { role: "user", content: [...parts] }];
3643
+ }
3644
+ async function extractErrorDetail(response) {
3645
+ try {
3646
+ const body = await response.json();
3647
+ return body.error;
3648
+ } catch {
3649
+ return void 0;
3650
+ }
3651
+ }
3652
+ async function* parseSseEvent(rawEvent, toolCallAccumulator) {
3653
+ for (const line of rawEvent.split("\n")) {
3654
+ if (!line.startsWith("data:")) continue;
3655
+ const payload = line.slice(5).trim();
3656
+ if (payload.length === 0 || payload === "[DONE]") continue;
3657
+ let chunk;
3658
+ try {
3659
+ chunk = JSON.parse(payload);
3660
+ } catch {
3661
+ continue;
3662
+ }
3663
+ const delta = chunk.choices?.[0]?.delta;
3664
+ if (delta?.reasoning) {
3665
+ yield { type: "delta", channel: "reasoning", text: delta.reasoning };
3666
+ }
3667
+ if (delta?.content) {
3668
+ yield { type: "delta", channel: "answer", text: delta.content };
3669
+ }
3670
+ if (delta?.tool_calls) {
3671
+ for (const toolCallDelta of delta.tool_calls) toolCallAccumulator.push(toolCallDelta);
3672
+ }
3673
+ if (chunk.usage) {
3674
+ yield {
3675
+ type: "usage",
3676
+ usage: {
3677
+ inputTokens: chunk.usage.prompt_tokens ?? 0,
3678
+ outputTokens: chunk.usage.completion_tokens ?? 0,
3679
+ reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens
3680
+ }
3681
+ };
3682
+ }
3683
+ }
3684
+ }
3685
+ async function* parseSseStream(body) {
3686
+ const reader = body.getReader();
3687
+ const decoder = new TextDecoder();
3688
+ const toolCallAccumulator = new ToolCallAccumulator();
3689
+ let buffer = "";
3690
+ try {
3691
+ while (true) {
3692
+ const { done, value } = await reader.read();
3693
+ if (done) break;
3694
+ buffer += decoder.decode(value, { stream: true });
3695
+ let separator = buffer.indexOf("\n\n");
3696
+ while (separator !== -1) {
3697
+ const rawEvent = buffer.slice(0, separator);
3698
+ buffer = buffer.slice(separator + 2);
3699
+ separator = buffer.indexOf("\n\n");
3700
+ yield* parseSseEvent(rawEvent, toolCallAccumulator);
3701
+ }
3702
+ }
3703
+ } catch (err) {
3704
+ throw errorFromUnknown(err);
3705
+ } finally {
3706
+ reader.releaseLock();
3707
+ }
3708
+ const calls = toolCallAccumulator.finish();
3709
+ if (calls.length > 0) yield { type: "tool_calls", calls };
3710
+ }
3711
+ function createOpenAiCompatibleAdapter(config) {
3712
+ if (!config.baseUrl && !config.endpointUrl) {
3713
+ throw new Error("createOpenAiCompatibleAdapter requires baseUrl or endpointUrl");
3714
+ }
3715
+ const endpoint = config.endpointUrl ?? `${config.baseUrl}/chat/completions`;
3716
+ return {
3717
+ async *stream(req) {
3718
+ const timeout = AbortSignal.timeout(config.timeoutMs);
3719
+ const signal = req.signal ? AbortSignal.any([req.signal, timeout]) : timeout;
3720
+ let response;
3721
+ try {
3722
+ response = await fetch(endpoint, {
3723
+ method: "POST",
3724
+ headers: {
3725
+ Authorization: `Bearer ${config.apiKey}`,
3726
+ "Content-Type": "application/json"
3727
+ },
3728
+ body: JSON.stringify({
3729
+ model: req.externalModelId,
3730
+ messages: mergeImages(req.messages.map(toWireMessage), req.images ?? []),
3731
+ temperature: req.temperature,
3732
+ max_completion_tokens: req.maxOutputTokens,
3733
+ top_p: req.topP,
3734
+ stream: true,
3735
+ stream_options: { include_usage: true },
3736
+ ...req.tools && req.tools.length > 0 ? {
3737
+ tools: req.tools.map((tool) => ({ type: "function", function: tool })),
3738
+ tool_choice: "auto"
3739
+ } : {}
3740
+ }),
3741
+ signal
3742
+ });
3743
+ } catch (err) {
3744
+ throw errorFromUnknown(err);
3745
+ }
3746
+ if (!response.ok || !response.body) {
3747
+ const detail = await extractErrorDetail(response);
3748
+ throw errorFromStatus(response.status, detail?.message, detail);
3749
+ }
3750
+ yield* parseSseStream(response.body);
3751
+ }
3752
+ };
3753
+ }
3754
+
3755
+ // src/providers/openai-compatible/openai-compatible.provider.ts
3756
+ var DEFAULT_TIMEOUT_MS = 3e4;
3757
+ var STRUCTURED_TEMPERATURE2 = 0.1;
3758
+ var OpenAiCompatibleProvider = class {
3759
+ constructor(config) {
3760
+ this.config = config;
3761
+ if (!config.baseUrl && !config.endpointUrl) {
3762
+ throw new AIError(
3763
+ "MISSING_CONFIG" /* MISSING_CONFIG */,
3764
+ "OpenAiCompatibleProvider requires baseUrl or endpointUrl"
3765
+ );
3766
+ }
3767
+ this.adapter = createOpenAiCompatibleAdapter({
3768
+ baseUrl: config.baseUrl,
3769
+ endpointUrl: config.endpointUrl,
3770
+ apiKey: config.apiKey,
3771
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
3772
+ });
3773
+ this.tiers = mergeTierOverrides(
3774
+ config.model ? { fast: config.model, balanced: config.model, powerful: config.model } : {},
3775
+ config.models
3776
+ );
3777
+ }
3778
+ config;
3779
+ id = "openai-compatible";
3780
+ name = "OpenAI-compatible";
3781
+ capabilities = ["text"];
3782
+ adapter;
3783
+ tiers;
3784
+ jsonParser = new JsonResponseParser();
3785
+ resolveModel(tier) {
3786
+ return resolveModel(this.tiers, tier, this.id);
3787
+ }
3788
+ async healthCheck() {
3789
+ return this.config.apiKey && (this.config.baseUrl || this.config.endpointUrl) ? "healthy" : "unavailable";
3790
+ }
3791
+ async estimateCost() {
3792
+ return 0;
3793
+ }
3794
+ async generate(request) {
3795
+ const model = this.modelFor(request);
3796
+ const outcome = await this.consume(this.toAdapterRequest(model, request));
3797
+ if (outcome.text.length === 0) {
3798
+ throw this.noContentError(model);
3799
+ }
3800
+ return {
3801
+ text: outcome.text,
3802
+ usage: outcome.usage,
3803
+ model,
3804
+ providerId: this.id,
3805
+ finishReason: "stop"
3806
+ };
3807
+ }
3808
+ async generateStructured(request) {
3809
+ const model = this.modelFor(request);
3810
+ const messages = this.toWireMessages(request.messages, request.systemPrompt);
3811
+ const lastUserIndex = findLastIndex4(messages, (m) => m.role === "user");
3812
+ if (lastUserIndex === -1) {
3813
+ throw new AIError(
3814
+ "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
3815
+ "generateStructured requires at least one user message"
3816
+ );
3817
+ }
3818
+ const structuredPrompt = new StructuredPromptBuilder(
3819
+ messages[lastUserIndex]?.content ?? ""
3820
+ ).withSchema(request.schema).build();
3821
+ messages[lastUserIndex] = { role: "user", content: structuredPrompt };
3822
+ const outcome = await this.consume({
3823
+ externalModelId: model,
3824
+ messages,
3825
+ temperature: STRUCTURED_TEMPERATURE2,
3826
+ signal: request.signal
3827
+ });
3828
+ if (outcome.text.length === 0) {
3829
+ throw this.noContentError(model);
3830
+ }
3831
+ return {
3832
+ value: this.jsonParser.parse(outcome.text),
3833
+ result: {
3834
+ text: outcome.text,
3835
+ usage: outcome.usage,
3836
+ model,
3837
+ providerId: this.id,
3838
+ finishReason: "stop"
3839
+ }
3840
+ };
3841
+ }
3842
+ async stream(request, callbacks) {
3843
+ const model = this.modelFor(request);
3844
+ try {
3845
+ const outcome = await this.consume(this.toAdapterRequest(model, request), (text) => {
3846
+ callbacks.onChunk?.(text);
3847
+ });
3848
+ if (outcome.text.length === 0) {
3849
+ throw this.noContentError(model);
3850
+ }
3851
+ const result = {
3852
+ text: outcome.text,
3853
+ usage: outcome.usage,
3854
+ model,
3855
+ providerId: this.id,
3856
+ finishReason: "stop"
3857
+ };
3858
+ callbacks.onComplete?.(result);
3859
+ return result;
3860
+ } catch (error) {
3861
+ callbacks.onError?.(error instanceof Error ? error : new Error(String(error)));
3862
+ throw error;
3863
+ }
3864
+ }
3865
+ /* ---------- internals ---------- */
3866
+ /** Drain one adapter stream to its end. `onChunk`, when given, sees answer
3867
+ * deltas as they arrive — the streaming surface is this same drain. */
3868
+ async consume(req, onChunk) {
3869
+ let text = "";
3870
+ let usage;
3871
+ for await (const event of this.adapter.stream(req)) {
3872
+ if (event.type === "delta" && event.channel === "answer") {
3873
+ text += event.text;
3874
+ onChunk?.(event.text);
3875
+ } else if (event.type === "usage") {
3876
+ usage = event.usage;
3877
+ }
3878
+ }
3879
+ return { text, usage };
3880
+ }
3881
+ modelFor(request) {
3882
+ return request.model ?? this.resolveModel(request.tier ?? "balanced");
3883
+ }
3884
+ noContentError(model) {
3885
+ return new AIError(
3886
+ "SERVER_ERROR" /* SERVER_ERROR */,
3887
+ "No content generated from the OpenAI-compatible endpoint",
3888
+ void 0,
3889
+ { model }
3890
+ );
3891
+ }
3892
+ toAdapterRequest(model, request) {
3893
+ return {
3894
+ externalModelId: model,
3895
+ messages: this.toWireMessages(request.messages, request.systemPrompt),
3896
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
3897
+ ...request.maxTokens !== void 0 ? { maxOutputTokens: request.maxTokens } : {},
3898
+ ...request.topP !== void 0 ? { topP: request.topP } : {},
3899
+ ...request.signal ? { signal: request.signal } : {}
3900
+ };
3901
+ }
3902
+ /** Neutral messages onto the adapter's shape; systemPrompt becomes the
3903
+ * leading system message (in-band, the OpenAI way). */
3904
+ toWireMessages(messages, systemPrompt) {
3905
+ const wire = [];
3906
+ if (systemPrompt && systemPrompt.length > 0) {
3907
+ wire.push({ role: "system", content: systemPrompt });
3908
+ }
3909
+ for (const message of messages) {
3910
+ wire.push({ role: message.role, content: message.content });
3911
+ }
3912
+ return wire;
3913
+ }
3914
+ };
3915
+ function findLastIndex4(items, predicate) {
3916
+ for (let index = items.length - 1; index >= 0; index -= 1) {
3917
+ if (predicate(items[index])) return index;
3918
+ }
3919
+ return -1;
3920
+ }
3921
+
3922
+ // src/providers/workers-ai/workers-ai.entities.ts
3923
+ var WorkersAIError = class extends Error {
3924
+ status;
3925
+ constructor(message, status) {
3926
+ super(message);
3927
+ this.name = "WorkersAIError";
3928
+ this.status = status;
3929
+ }
3930
+ };
3931
+
3932
+ // src/providers/workers-ai/workers-ai.text.ts
3933
+ var WORKERS_AI_LLM_MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast";
3934
+ var WORKERS_AI_TEXT_TIMEOUT_MS = 25e3;
3935
+ function requireAi(env) {
3936
+ const ai = env.AI;
3937
+ if (!ai || typeof ai.run !== "function") throw new WorkersAIError("Workers AI is not configured");
3938
+ return ai;
3939
+ }
3940
+ async function runWorkersAIText(env, messages, opts = {}) {
3941
+ const ai = requireAi(env);
3942
+ const model = opts.model ?? WORKERS_AI_LLM_MODEL;
3943
+ let result;
3944
+ try {
3945
+ result = await Promise.race([
3946
+ ai.run(model, {
3947
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
3948
+ max_tokens: opts.maxTokens ?? 900,
3949
+ temperature: opts.temperature ?? 0.7
3950
+ }),
3951
+ new Promise(
3952
+ (_, reject) => setTimeout(() => reject(new WorkersAIError("Workers AI timed out")), WORKERS_AI_TEXT_TIMEOUT_MS)
3953
+ )
3954
+ ]);
3955
+ } catch (err) {
3956
+ throw err instanceof WorkersAIError ? err : new WorkersAIError(err instanceof Error ? err.message : "Workers AI request failed");
3957
+ }
3958
+ const response = result ?? {};
3959
+ let text = response.response;
3960
+ if (typeof text !== "string" || text.length === 0) {
3961
+ text = response.choices?.[0]?.message?.content;
3962
+ }
3963
+ if (typeof text !== "string" || text.length === 0) {
3964
+ throw new WorkersAIError("Workers AI returned no text");
3965
+ }
3966
+ return text;
3967
+ }
3968
+ async function* streamWorkersAIText(env, messages, opts = {}) {
3969
+ const ai = requireAi(env);
3970
+ const model = opts.model ?? WORKERS_AI_LLM_MODEL;
3971
+ let body;
3972
+ try {
3973
+ body = await Promise.race([
3974
+ ai.run(model, {
3975
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
3976
+ stream: true,
3977
+ max_tokens: opts.maxTokens ?? 600,
3978
+ temperature: opts.temperature ?? 0.7
3979
+ }),
3980
+ new Promise(
3981
+ (_, reject) => setTimeout(() => reject(new WorkersAIError("Workers AI timed out")), WORKERS_AI_TEXT_TIMEOUT_MS)
3982
+ )
3983
+ ]);
3984
+ } catch (err) {
3985
+ throw err instanceof WorkersAIError ? err : new WorkersAIError(err instanceof Error ? err.message : "Workers AI request failed");
3986
+ }
3987
+ const stream = body;
3988
+ const getReader = typeof stream === "object" && stream !== null && "getReader" in stream && typeof stream.getReader === "function" ? stream.getReader : void 0;
3989
+ if (!getReader) throw new WorkersAIError("Workers AI stream was not readable");
3990
+ const reader = getReader.call(stream);
3991
+ const decoder = new TextDecoder();
3992
+ let buffer = "";
3993
+ try {
3994
+ for (; ; ) {
3995
+ const { done, value } = await reader.read();
3996
+ if (done) break;
3997
+ buffer += decoder.decode(value, { stream: true });
3998
+ let newlineAt;
3999
+ while ((newlineAt = buffer.indexOf("\n")) !== -1) {
4000
+ let line = buffer.slice(0, newlineAt);
4001
+ buffer = buffer.slice(newlineAt + 1);
4002
+ if (line.endsWith("\r")) line = line.slice(0, -1);
4003
+ if (line.startsWith(":") || line.trim() === "") continue;
4004
+ if (!line.startsWith("data: ")) continue;
4005
+ const payload = line.slice(6).trim();
4006
+ if (payload === "[DONE]") return;
4007
+ try {
4008
+ const parsed = JSON.parse(payload);
4009
+ if (typeof parsed.response === "string" && parsed.response.length > 0) yield parsed.response;
4010
+ } catch {
4011
+ buffer = `${line}
4012
+ ${buffer}`;
4013
+ break;
4014
+ }
4015
+ }
4016
+ }
4017
+ } finally {
4018
+ reader.releaseLock();
4019
+ }
4020
+ }
4021
+
4022
+ // src/providers/workers-ai/workers-ai.provider.ts
4023
+ var WORKERS_AI_TIERS = {
4024
+ fast: "@cf/meta/llama-3.1-8b-instruct-fp8-fast",
4025
+ balanced: WORKERS_AI_LLM_MODEL,
4026
+ powerful: WORKERS_AI_LLM_MODEL
4027
+ };
4028
+ var WorkersAIProvider = class {
4029
+ id = "workers-ai";
4030
+ name = "Cloudflare Workers AI";
4031
+ capabilities = ["text"];
4032
+ binding;
4033
+ tiers;
4034
+ jsonParser = new JsonResponseParser();
4035
+ constructor(config) {
4036
+ this.binding = config.binding;
4037
+ this.tiers = mergeTierOverrides(WORKERS_AI_TIERS, config.models);
4038
+ }
4039
+ resolveModel(tier) {
4040
+ return resolveModel(this.tiers, tier, this.id);
4041
+ }
4042
+ async healthCheck() {
4043
+ return typeof this.binding?.run === "function" ? "healthy" : "unavailable";
4044
+ }
4045
+ /** Workers AI is allocation-priced (neurons per deployment), not per
4046
+ * request — there is no per-call cost to estimate. */
4047
+ async estimateCost(_request) {
4048
+ return 0;
4049
+ }
4050
+ async generate(request) {
4051
+ const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
4052
+ const messages = withSystemPrompt(request);
4053
+ try {
4054
+ const text = await runWorkersAIText(
4055
+ { AI: this.binding },
4056
+ messages,
4057
+ {
4058
+ model,
4059
+ maxTokens: request.maxTokens,
4060
+ temperature: request.temperature
4061
+ }
4062
+ );
4063
+ return { text, model, providerId: this.id, finishReason: "stop" };
4064
+ } catch (error) {
4065
+ throw toAIError(error);
4066
+ }
4067
+ }
4068
+ async generateStructured(request) {
4069
+ const messages = [...request.messages];
4070
+ const lastUserIndex = findLastIndex5(messages, (m) => m.role === "user");
4071
+ if (lastUserIndex === -1) {
4072
+ throw new AIError("UNKNOWN_ERROR" /* UNKNOWN_ERROR */, "generateStructured requires at least one user message");
4073
+ }
4074
+ messages[lastUserIndex] = {
4075
+ role: "user",
4076
+ content: new StructuredPromptBuilder(messages[lastUserIndex]?.content ?? "").withSchema(request.schema).build()
4077
+ };
4078
+ const result = await this.generate({ ...request, messages, temperature: 0.1 });
4079
+ return { value: this.jsonParser.parse(result.text), result };
4080
+ }
4081
+ async stream(request, callbacks) {
4082
+ const model = request.model ?? this.resolveModel(request.tier ?? "balanced");
4083
+ const messages = withSystemPrompt(request);
4084
+ let text = "";
4085
+ try {
4086
+ for await (const chunk of streamWorkersAIText({ AI: this.binding }, messages, {
4087
+ model,
4088
+ maxTokens: request.maxTokens,
4089
+ temperature: request.temperature
4090
+ })) {
4091
+ text += chunk;
4092
+ callbacks.onChunk?.(chunk);
4093
+ }
4094
+ } catch (error) {
4095
+ const aiError = toAIError(error);
4096
+ callbacks.onError?.(aiError);
4097
+ throw aiError;
4098
+ }
4099
+ const result = { text, model, providerId: this.id, finishReason: "stop" };
4100
+ callbacks.onComplete?.(result);
4101
+ return result;
4102
+ }
4103
+ };
4104
+ function withSystemPrompt(request) {
4105
+ if (request.systemPrompt && request.systemPrompt.length > 0) {
4106
+ return [
4107
+ { role: "system", content: request.systemPrompt },
4108
+ ...request.messages.filter((m) => m.role !== "system")
4109
+ ];
4110
+ }
4111
+ return request.messages;
4112
+ }
4113
+ function toAIError(error) {
4114
+ if (error instanceof AIError) return error;
4115
+ const message = error instanceof Error ? error.message : "Workers AI request failed";
4116
+ const type = error instanceof WorkersAIError && error.message.includes("not configured") ? "INVALID_API_KEY" /* INVALID_API_KEY */ : "SERVER_ERROR" /* SERVER_ERROR */;
4117
+ return new AIError(type, message, error instanceof Error ? error : void 0);
4118
+ }
4119
+ function findLastIndex5(items, predicate) {
4120
+ for (let i = items.length - 1; i >= 0; i--) {
4121
+ const item = items[i];
4122
+ if (item !== void 0 && predicate(item)) return i;
4123
+ }
4124
+ return -1;
4125
+ }
4126
+
3522
4127
  // src/providers/factory/create-ai.ts
3523
4128
  function createAI(config = {}) {
3524
4129
  const factory = new ProviderFactory({
@@ -3541,6 +4146,15 @@ function createAI(config = {}) {
3541
4146
  if (config.pruna?.apiKey) {
3542
4147
  factory.register(new PrunaProvider({ ...config.pruna, enabled: true }));
3543
4148
  }
4149
+ const openaiCompatible = config.openaiCompatible;
4150
+ if (openaiCompatible?.apiKey && (openaiCompatible.baseUrl || openaiCompatible.endpointUrl)) {
4151
+ factory.register(
4152
+ new OpenAiCompatibleProvider({ ...openaiCompatible, apiKey: openaiCompatible.apiKey })
4153
+ );
4154
+ }
4155
+ if (config.workersAI?.binding) {
4156
+ factory.register(new WorkersAIProvider(config.workersAI));
4157
+ }
3544
4158
  return factory;
3545
4159
  }
3546
4160
 
@@ -3997,7 +4611,7 @@ async function batchRequest(requests, options = {}) {
3997
4611
  errors.push(result.reason);
3998
4612
  if (stopOnError) {
3999
4613
  throw new Error(
4000
- `Batch request failed: ${errors[0].message}`
4614
+ `Batch request failed: ${errors[0]?.message}`
4001
4615
  );
4002
4616
  }
4003
4617
  }
@@ -4562,7 +5176,7 @@ function safeJSONParse(text, fallback, onError) {
4562
5176
  }
4563
5177
  if (error instanceof SyntaxError) {
4564
5178
  const positionMatch = error.message.match(/position (\d+)/);
4565
- const position = positionMatch ? parseInt(positionMatch[1], 10) : -1;
5179
+ const position = positionMatch ? parseInt(positionMatch[1] ?? "", 10) : -1;
4566
5180
  if (position > 0) {
4567
5181
  text.substring(Math.max(0, position - 50), position + 50);
4568
5182
  }
@@ -5230,7 +5844,9 @@ function useAIGeneration(options) {
5230
5844
  setProgress({ stage: "uploading", progress: 0, message: "Uploading files..." });
5231
5845
  const urls = [];
5232
5846
  for (let i = 0; i < files.length; i++) {
5233
- const url = await uploadPhoto(files[i], userId);
5847
+ const file = files[i];
5848
+ if (!file) continue;
5849
+ const url = await uploadPhoto(file, userId);
5234
5850
  urls.push(url);
5235
5851
  setProgress({
5236
5852
  stage: "uploading",
@@ -6034,7 +6650,7 @@ function useChat(options, deps) {
6034
6650
  if (streamEnabled) {
6035
6651
  const placeholderId = chatIdGenerator.next("assistant");
6036
6652
  streamingPlaceholderId = placeholderId;
6037
- const placeholder = {
6653
+ const placeholder2 = {
6038
6654
  id: placeholderId,
6039
6655
  sender: "assistant",
6040
6656
  content: "",
@@ -6042,7 +6658,7 @@ function useChat(options, deps) {
6042
6658
  type: "text",
6043
6659
  metadata: { streaming: true }
6044
6660
  };
6045
- setMessages((prev) => [...prev, placeholder]);
6661
+ setMessages((prev) => [...prev, placeholder2]);
6046
6662
  const fullText = await chatService.generateStreamingResponse(
6047
6663
  trimmed,
6048
6664
  contextForAI,
@@ -6056,9 +6672,9 @@ function useChat(options, deps) {
6056
6672
  controller.signal
6057
6673
  );
6058
6674
  const finalMessage = {
6059
- ...placeholder,
6675
+ ...placeholder2,
6060
6676
  content: fullText,
6061
- metadata: { ...placeholder.metadata, streaming: false }
6677
+ metadata: { ...placeholder2.metadata, streaming: false }
6062
6678
  };
6063
6679
  setMessages(
6064
6680
  (prev) => prev.map((m) => m.id === placeholderId ? finalMessage : m)
@@ -6106,7 +6722,7 @@ function useChat(options, deps) {
6106
6722
  if (currentMessages.length === 0) return null;
6107
6723
  const lastUserMessage = [...currentMessages].reverse().find((m) => m.sender === "user");
6108
6724
  if (!lastUserMessage) return null;
6109
- const trimmed = currentMessages.length > 0 && currentMessages[currentMessages.length - 1].sender === "assistant" ? currentMessages.slice(0, -1) : currentMessages;
6725
+ const trimmed = currentMessages.length > 0 && currentMessages[currentMessages.length - 1]?.sender === "assistant" ? currentMessages.slice(0, -1) : currentMessages;
6110
6726
  const controller = new AbortController();
6111
6727
  abortControllerRef.current?.abort();
6112
6728
  abortControllerRef.current = controller;
@@ -6121,7 +6737,7 @@ function useChat(options, deps) {
6121
6737
  controller.signal
6122
6738
  );
6123
6739
  setMessages((prev) => {
6124
- const lastIsAI = prev.length > 0 && prev[prev.length - 1].sender === "assistant";
6740
+ const lastIsAI = prev.length > 0 && prev[prev.length - 1]?.sender === "assistant";
6125
6741
  const base = lastIsAI ? prev.slice(0, -1) : prev;
6126
6742
  return [...base, aiResponse];
6127
6743
  });
@@ -6323,6 +6939,120 @@ function useGroq(options = {}, deps) {
6323
6939
  clearError
6324
6940
  };
6325
6941
  }
6942
+ var REMOTE_CHAT_STREAMING_ID = "streaming";
6943
+ function placeholder(id, status) {
6944
+ return { id, role: "assistant", content: "", reasoning: "", status, error: null, usage: null };
6945
+ }
6946
+ function useRemoteChat(options) {
6947
+ const { client } = options;
6948
+ const [messages, setMessages] = react.useState([]);
6949
+ const [isStreaming, setIsStreaming] = react.useState(false);
6950
+ const [isResolving, setIsResolving] = react.useState(false);
6951
+ const handleRef = react.useRef(null);
6952
+ const stoppedRef = react.useRef(false);
6953
+ const optionsRef = react.useRef(options);
6954
+ optionsRef.current = options;
6955
+ const patchLast = react.useCallback((patch) => {
6956
+ setMessages((prev) => {
6957
+ if (prev.length === 0) return prev;
6958
+ const next = [...prev];
6959
+ const last = next[next.length - 1];
6960
+ if (!last) return prev;
6961
+ next[next.length - 1] = { ...last, ...patch };
6962
+ return next;
6963
+ });
6964
+ }, []);
6965
+ const appendDelta = react.useCallback((channel, text) => {
6966
+ setMessages((prev) => {
6967
+ if (prev.length === 0) return prev;
6968
+ const next = [...prev];
6969
+ const last = next[next.length - 1];
6970
+ if (!last) return prev;
6971
+ next[next.length - 1] = channel === "reasoning" ? { ...last, reasoning: last.reasoning + text } : { ...last, content: last.content + text };
6972
+ return next;
6973
+ });
6974
+ }, []);
6975
+ const callbacks = react.useCallback(
6976
+ () => ({
6977
+ onDelta: appendDelta,
6978
+ onUsage: (usage) => patchLast({ usage }),
6979
+ onError: (code, message) => patchLast({ status: "error", error: { code, message } }),
6980
+ onDone: (messageId) => patchLast({ id: messageId, status: "complete" }),
6981
+ onToolCall: (toolCall) => optionsRef.current.onToolCall?.(toolCall)
6982
+ }),
6983
+ [appendDelta, patchLast]
6984
+ );
6985
+ const stream = react.useCallback(
6986
+ async (conversationId, body, opts) => {
6987
+ setIsStreaming(true);
6988
+ stoppedRef.current = false;
6989
+ setMessages(() => [
6990
+ ...opts?.localUserContent !== void 0 ? [
6991
+ {
6992
+ id: optionsRef.current.localUserId?.() ?? `local-${Date.now()}`,
6993
+ role: "user",
6994
+ content: opts.localUserContent,
6995
+ reasoning: "",
6996
+ status: "complete",
6997
+ error: null,
6998
+ usage: null
6999
+ }
7000
+ ] : [],
7001
+ placeholder(REMOTE_CHAT_STREAMING_ID, "streaming")
7002
+ ]);
7003
+ const handle = client.streamChat(conversationId, body, callbacks());
7004
+ handleRef.current = handle;
7005
+ try {
7006
+ await handle.promise.catch(() => void 0);
7007
+ } finally {
7008
+ handleRef.current = null;
7009
+ setIsStreaming(false);
7010
+ await optionsRef.current.onStreamEnd?.({
7011
+ conversationId,
7012
+ stopped: stoppedRef.current
7013
+ });
7014
+ stoppedRef.current = false;
7015
+ }
7016
+ },
7017
+ [callbacks, client]
7018
+ );
7019
+ const resolveToolCall = react.useCallback(
7020
+ async (conversationId, toolCallId, approved) => {
7021
+ setIsResolving(true);
7022
+ setIsStreaming(true);
7023
+ stoppedRef.current = false;
7024
+ setMessages((prev) => [...prev, placeholder(REMOTE_CHAT_STREAMING_ID, "streaming")]);
7025
+ try {
7026
+ return await client.resolveToolCall(conversationId, toolCallId, approved, callbacks());
7027
+ } catch {
7028
+ return false;
7029
+ } finally {
7030
+ setIsStreaming(false);
7031
+ setIsResolving(false);
7032
+ await optionsRef.current.onStreamEnd?.({
7033
+ conversationId,
7034
+ stopped: stoppedRef.current
7035
+ });
7036
+ stoppedRef.current = false;
7037
+ }
7038
+ },
7039
+ [callbacks, client]
7040
+ );
7041
+ const abort = react.useCallback(() => {
7042
+ stoppedRef.current = true;
7043
+ handleRef.current?.abort();
7044
+ }, []);
7045
+ return {
7046
+ messages,
7047
+ setMessages,
7048
+ isStreaming,
7049
+ isResolving,
7050
+ stream,
7051
+ resolveToolCall,
7052
+ abort,
7053
+ patchLast
7054
+ };
7055
+ }
6326
7056
  function isAbortError2(err) {
6327
7057
  if (!(err instanceof Error)) return false;
6328
7058
  if (err.name === "AbortError") return true;
@@ -6438,6 +7168,7 @@ function usePrunaProxy(options) {
6438
7168
 
6439
7169
  exports.CancellationError = CancellationError;
6440
7170
  exports.GENERATION_COSTS = CREDIT_COSTS;
7171
+ exports.REMOTE_CHAT_STREAMING_ID = REMOTE_CHAT_STREAMING_ID;
6441
7172
  exports.calculateCreditCost = calculateContentCreditCost;
6442
7173
  exports.useAIContent = useAIContent;
6443
7174
  exports.useAIGeneration = useAIGeneration;
@@ -6450,6 +7181,7 @@ exports.useGroq = useGroq;
6450
7181
  exports.useImageWizard = useImageWizard;
6451
7182
  exports.usePrunaGeneration = usePrunaGeneration;
6452
7183
  exports.usePrunaProxy = usePrunaProxy;
7184
+ exports.useRemoteChat = useRemoteChat;
6453
7185
  exports.useSocialContentGenerator = useSocialContentGenerator;
6454
7186
  exports.useText = useText;
6455
7187
  exports.useVideoWizard = useVideoWizard;