@jaypie/llm 1.3.3 → 1.3.4
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.
- package/dist/cjs/index.cjs +741 -119
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +1 -1
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +8 -1
- package/dist/cjs/providers/anthropic/client.d.ts +54 -0
- package/dist/cjs/providers/anthropic/types.d.ts +127 -0
- package/dist/cjs/providers/anthropic/utils.d.ts +5 -5
- package/dist/cjs/providers/google/client.d.ts +44 -0
- package/dist/cjs/providers/google/utils.d.ts +2 -3
- package/dist/cjs/providers/openai/client.d.ts +74 -0
- package/dist/cjs/providers/openai/responseFormat.d.ts +17 -0
- package/dist/cjs/providers/openai/utils.d.ts +4 -4
- package/dist/cjs/providers/openrouter/client.d.ts +40 -0
- package/dist/cjs/providers/openrouter/utils.d.ts +2 -3
- package/dist/cjs/providers/xai/utils.d.ts +2 -2
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/cjs/util/sse.d.ts +12 -0
- package/dist/esm/index.js +734 -112
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +1 -1
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +8 -1
- package/dist/esm/providers/anthropic/client.d.ts +54 -0
- package/dist/esm/providers/anthropic/types.d.ts +127 -0
- package/dist/esm/providers/anthropic/utils.d.ts +5 -5
- package/dist/esm/providers/google/client.d.ts +44 -0
- package/dist/esm/providers/google/utils.d.ts +2 -3
- package/dist/esm/providers/openai/client.d.ts +74 -0
- package/dist/esm/providers/openai/responseFormat.d.ts +17 -0
- package/dist/esm/providers/openai/utils.d.ts +4 -4
- package/dist/esm/providers/openrouter/client.d.ts +40 -0
- package/dist/esm/providers/openrouter/utils.d.ts +2 -3
- package/dist/esm/providers/xai/utils.d.ts +2 -2
- package/dist/esm/util/index.d.ts +1 -0
- package/dist/esm/util/sse.d.ts +12 -0
- package/package.json +2 -18
package/dist/cjs/index.cjs
CHANGED
|
@@ -5,8 +5,6 @@ var log$1 = require('@jaypie/logger');
|
|
|
5
5
|
var v4 = require('zod/v4');
|
|
6
6
|
var kit = require('@jaypie/kit');
|
|
7
7
|
var RandomLib = require('random');
|
|
8
|
-
var openai = require('openai');
|
|
9
|
-
var zod = require('openai/helpers/zod');
|
|
10
8
|
var module$1 = require('module');
|
|
11
9
|
var url = require('url');
|
|
12
10
|
var pdfLib = require('pdf-lib');
|
|
@@ -1088,6 +1086,45 @@ function repairFormatKeys({ content, format, }) {
|
|
|
1088
1086
|
return repairFromSchema(schema, structuredClone(content));
|
|
1089
1087
|
}
|
|
1090
1088
|
|
|
1089
|
+
const DEFAULT_DONE_SENTINEL = "[DONE]";
|
|
1090
|
+
/**
|
|
1091
|
+
* Parse a Server-Sent Events stream into decoded JSON chunks. Buffers across
|
|
1092
|
+
* network reads, dispatches on newline-delimited `data:` lines, and stops at
|
|
1093
|
+
* the done sentinel (OpenAI-style `[DONE]`; Gemini omits it and simply ends
|
|
1094
|
+
* the stream). Comment lines (`: ...`, used for keep-alive) are ignored.
|
|
1095
|
+
*
|
|
1096
|
+
* Shared by the provider HTTP clients that replaced their vendor SDKs.
|
|
1097
|
+
*/
|
|
1098
|
+
async function* parseSseStream(body, { doneSentinel = DEFAULT_DONE_SENTINEL } = {}) {
|
|
1099
|
+
const reader = body.getReader();
|
|
1100
|
+
const decoder = new TextDecoder();
|
|
1101
|
+
let buffer = "";
|
|
1102
|
+
try {
|
|
1103
|
+
for (;;) {
|
|
1104
|
+
const { done, value } = await reader.read();
|
|
1105
|
+
if (done)
|
|
1106
|
+
break;
|
|
1107
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1108
|
+
let newlineIndex;
|
|
1109
|
+
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
1110
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
1111
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
1112
|
+
if (line === "" || line.startsWith(":"))
|
|
1113
|
+
continue;
|
|
1114
|
+
if (!line.startsWith("data:"))
|
|
1115
|
+
continue;
|
|
1116
|
+
const data = line.slice("data:".length).trim();
|
|
1117
|
+
if (data === doneSentinel)
|
|
1118
|
+
return;
|
|
1119
|
+
yield JSON.parse(data);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
finally {
|
|
1124
|
+
reader.releaseLock();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1091
1128
|
/**
|
|
1092
1129
|
* Helper function to safely call a function that might throw
|
|
1093
1130
|
* @param fn Function to call safely
|
|
@@ -1615,7 +1652,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1615
1652
|
? [...request.tools]
|
|
1616
1653
|
: [];
|
|
1617
1654
|
if (useFallbackStructuredOutput && request.format) {
|
|
1618
|
-
log$1.warn(`[AnthropicAdapter] Using legacy structured_output tool fallback for model ${anthropicRequest.model}; native output_config previously rejected for this model.`);
|
|
1655
|
+
log$1.log.warn(`[AnthropicAdapter] Using legacy structured_output tool fallback for model ${anthropicRequest.model}; native output_config previously rejected for this model.`);
|
|
1619
1656
|
allTools.push({
|
|
1620
1657
|
name: STRUCTURED_OUTPUT_TOOL_NAME$3,
|
|
1621
1658
|
description: "Output a structured JSON object, " +
|
|
@@ -1730,7 +1767,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1730
1767
|
if (wantsStructuredOutput && isStructuredOutputUnsupportedError$1(error)) {
|
|
1731
1768
|
const model = anthropicRequest.model;
|
|
1732
1769
|
this.rememberModelRejectsStructuredOutput(model);
|
|
1733
|
-
log$1.warn(`[AnthropicAdapter] Model ${model} rejected native output_config; falling back to legacy structured_output tool emulation.`);
|
|
1770
|
+
log$1.log.warn(`[AnthropicAdapter] Model ${model} rejected native output_config; falling back to legacy structured_output tool emulation.`);
|
|
1734
1771
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(anthropicRequest);
|
|
1735
1772
|
return (await anthropic.messages.create(fallbackRequest, signal ? { signal } : undefined));
|
|
1736
1773
|
}
|
|
@@ -2793,7 +2830,7 @@ class GoogleAdapter extends BaseProviderAdapter {
|
|
|
2793
2830
|
? [...request.tools]
|
|
2794
2831
|
: [];
|
|
2795
2832
|
if (request.format && hasUserTools && !useNativeCombo) {
|
|
2796
|
-
log$1.warn(`[GoogleAdapter] Using legacy structured_output tool fallback for model ${geminiRequest.model}; native responseJsonSchema + tools combo is only available on Gemini 3.`);
|
|
2833
|
+
log$1.log.warn(`[GoogleAdapter] Using legacy structured_output tool fallback for model ${geminiRequest.model}; native responseJsonSchema + tools combo is only available on Gemini 3.`);
|
|
2797
2834
|
allTools.push({
|
|
2798
2835
|
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
2799
2836
|
description: "Output a structured JSON object, " +
|
|
@@ -2924,7 +2961,7 @@ class GoogleAdapter extends BaseProviderAdapter {
|
|
|
2924
2961
|
if (wantsNativeCombo && isStructuredOutputComboUnsupportedError(error)) {
|
|
2925
2962
|
const model = geminiRequest.model;
|
|
2926
2963
|
this.rememberModelRejectsStructuredOutputCombo(model);
|
|
2927
|
-
log$1.warn(`[GoogleAdapter] Model ${model} rejected native responseJsonSchema + tools combo; falling back to legacy structured_output tool emulation.`);
|
|
2964
|
+
log$1.log.warn(`[GoogleAdapter] Model ${model} rejected native responseJsonSchema + tools combo; falling back to legacy structured_output tool emulation.`);
|
|
2928
2965
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(geminiRequest);
|
|
2929
2966
|
const response = await genAI.models.generateContent({
|
|
2930
2967
|
model: fallbackRequest.model,
|
|
@@ -3508,6 +3545,206 @@ class GoogleAdapter extends BaseProviderAdapter {
|
|
|
3508
3545
|
// Export singleton instance
|
|
3509
3546
|
const googleAdapter = new GoogleAdapter();
|
|
3510
3547
|
|
|
3548
|
+
//
|
|
3549
|
+
//
|
|
3550
|
+
// Constants
|
|
3551
|
+
//
|
|
3552
|
+
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
3553
|
+
//
|
|
3554
|
+
//
|
|
3555
|
+
// Errors
|
|
3556
|
+
//
|
|
3557
|
+
// The adapter's `classifyError` uses `instanceof` against these classes, so the
|
|
3558
|
+
// client throws them directly. Status → class mapping mirrors the SDK; the
|
|
3559
|
+
// non-HTTP classes (connection/abort) are thrown when `fetch` itself rejects.
|
|
3560
|
+
//
|
|
3561
|
+
class OpenAiApiError extends Error {
|
|
3562
|
+
constructor(status, message, error) {
|
|
3563
|
+
super(message);
|
|
3564
|
+
this.name = this.constructor.name;
|
|
3565
|
+
this.status = status;
|
|
3566
|
+
this.error = error;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
let BadRequestError$1 = class BadRequestError extends OpenAiApiError {
|
|
3570
|
+
};
|
|
3571
|
+
let AuthenticationError$1 = class AuthenticationError extends OpenAiApiError {
|
|
3572
|
+
};
|
|
3573
|
+
let PermissionDeniedError$1 = class PermissionDeniedError extends OpenAiApiError {
|
|
3574
|
+
};
|
|
3575
|
+
let NotFoundError$1 = class NotFoundError extends OpenAiApiError {
|
|
3576
|
+
};
|
|
3577
|
+
class ConflictError extends OpenAiApiError {
|
|
3578
|
+
}
|
|
3579
|
+
class UnprocessableEntityError extends OpenAiApiError {
|
|
3580
|
+
}
|
|
3581
|
+
let RateLimitError$1 = class RateLimitError extends OpenAiApiError {
|
|
3582
|
+
};
|
|
3583
|
+
let InternalServerError$1 = class InternalServerError extends OpenAiApiError {
|
|
3584
|
+
};
|
|
3585
|
+
class APIConnectionError extends OpenAiApiError {
|
|
3586
|
+
}
|
|
3587
|
+
class APIConnectionTimeoutError extends APIConnectionError {
|
|
3588
|
+
}
|
|
3589
|
+
class APIUserAbortError extends OpenAiApiError {
|
|
3590
|
+
}
|
|
3591
|
+
function errorForStatus$1(status, message, error) {
|
|
3592
|
+
if (status === 400)
|
|
3593
|
+
return new BadRequestError$1(status, message, error);
|
|
3594
|
+
if (status === 401)
|
|
3595
|
+
return new AuthenticationError$1(status, message, error);
|
|
3596
|
+
if (status === 403)
|
|
3597
|
+
return new PermissionDeniedError$1(status, message, error);
|
|
3598
|
+
if (status === 404)
|
|
3599
|
+
return new NotFoundError$1(status, message, error);
|
|
3600
|
+
if (status === 409)
|
|
3601
|
+
return new ConflictError(status, message, error);
|
|
3602
|
+
if (status === 422) {
|
|
3603
|
+
return new UnprocessableEntityError(status, message, error);
|
|
3604
|
+
}
|
|
3605
|
+
if (status === 429)
|
|
3606
|
+
return new RateLimitError$1(status, message, error);
|
|
3607
|
+
if (status >= 500)
|
|
3608
|
+
return new InternalServerError$1(status, message, error);
|
|
3609
|
+
return new OpenAiApiError(status, message, error);
|
|
3610
|
+
}
|
|
3611
|
+
//
|
|
3612
|
+
//
|
|
3613
|
+
// Main
|
|
3614
|
+
//
|
|
3615
|
+
/**
|
|
3616
|
+
* Minimal `fetch`-based client for the OpenAI API. Replaces the `openai` SDK —
|
|
3617
|
+
* the adapters and provider utilities only need the Responses API
|
|
3618
|
+
* (`/responses`, streaming and non-streaming) for `operate`/`stream` and the
|
|
3619
|
+
* Chat Completions API (`/chat/completions`, plus a `parse` helper for
|
|
3620
|
+
* structured output) for `send`. Mirrors the SDK's `responses.create` and
|
|
3621
|
+
* `chat.completions.create` / `chat.completions.parse` surface so call sites are
|
|
3622
|
+
* unchanged. Also serves xAI via a custom `baseURL`.
|
|
3623
|
+
*/
|
|
3624
|
+
class OpenAIClient {
|
|
3625
|
+
constructor({ apiKey, baseURL = OPENAI_BASE_URL }) {
|
|
3626
|
+
this.apiKey = apiKey;
|
|
3627
|
+
this.baseURL = baseURL;
|
|
3628
|
+
this.responses = {
|
|
3629
|
+
create: ((params, options) => params.stream
|
|
3630
|
+
? this.createResponseStream(params, options)
|
|
3631
|
+
: this.createResponse(params, options)),
|
|
3632
|
+
};
|
|
3633
|
+
this.chat = {
|
|
3634
|
+
completions: {
|
|
3635
|
+
create: (params, options) => this.chatCompletion(params, options, { parse: false }),
|
|
3636
|
+
parse: (params, options) => this.chatCompletion(params, options, { parse: true }),
|
|
3637
|
+
},
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
headers(extra) {
|
|
3641
|
+
return {
|
|
3642
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
3643
|
+
"Content-Type": "application/json",
|
|
3644
|
+
...extra,
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
async post(path, body, { signal } = {}, { stream = false } = {}) {
|
|
3648
|
+
let response;
|
|
3649
|
+
try {
|
|
3650
|
+
response = await fetch(`${this.baseURL}${path}`, {
|
|
3651
|
+
method: "POST",
|
|
3652
|
+
headers: this.headers(stream ? { Accept: "text/event-stream" } : undefined),
|
|
3653
|
+
body: JSON.stringify(body),
|
|
3654
|
+
signal,
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
catch (cause) {
|
|
3658
|
+
if (signal?.aborted) {
|
|
3659
|
+
throw new APIUserAbortError(0, "Request was aborted");
|
|
3660
|
+
}
|
|
3661
|
+
const message = cause instanceof Error ? cause.message : "Connection error";
|
|
3662
|
+
const error = new APIConnectionError(0, message);
|
|
3663
|
+
error.cause = cause;
|
|
3664
|
+
throw error;
|
|
3665
|
+
}
|
|
3666
|
+
if (!response.ok)
|
|
3667
|
+
throw await this.toError(response);
|
|
3668
|
+
return response;
|
|
3669
|
+
}
|
|
3670
|
+
async toError(response) {
|
|
3671
|
+
let message = `OpenAI request failed with status ${response.status}`;
|
|
3672
|
+
let error;
|
|
3673
|
+
try {
|
|
3674
|
+
const body = (await response.json());
|
|
3675
|
+
if (body?.error?.message) {
|
|
3676
|
+
message = body.error.message;
|
|
3677
|
+
error = body.error;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
catch {
|
|
3681
|
+
// Non-JSON error body; keep the status-based message.
|
|
3682
|
+
}
|
|
3683
|
+
return errorForStatus$1(response.status, message, error);
|
|
3684
|
+
}
|
|
3685
|
+
async createResponse(params, options) {
|
|
3686
|
+
const response = await this.post("/responses", params, options);
|
|
3687
|
+
return (await response.json());
|
|
3688
|
+
}
|
|
3689
|
+
async createResponseStream(params, options) {
|
|
3690
|
+
const response = await this.post("/responses", params, options, {
|
|
3691
|
+
stream: true,
|
|
3692
|
+
});
|
|
3693
|
+
if (!response.body)
|
|
3694
|
+
return (async function* () { })();
|
|
3695
|
+
// The Responses API streams typed events and does not emit a `[DONE]`
|
|
3696
|
+
// sentinel; the empty default sentinel never matches, so the stream ends
|
|
3697
|
+
// when the connection closes.
|
|
3698
|
+
return parseSseStream(response.body);
|
|
3699
|
+
}
|
|
3700
|
+
async chatCompletion(params, options, { parse }) {
|
|
3701
|
+
const response = await this.post("/chat/completions", params, options);
|
|
3702
|
+
const json = (await response.json());
|
|
3703
|
+
// `chat.completions.parse` surfaces parsed JSON on each message; replicate
|
|
3704
|
+
// the SDK by JSON-parsing the assistant content into `message.parsed`.
|
|
3705
|
+
if (parse) {
|
|
3706
|
+
const choices = json.choices;
|
|
3707
|
+
if (Array.isArray(choices)) {
|
|
3708
|
+
for (const choice of choices) {
|
|
3709
|
+
const message = choice.message;
|
|
3710
|
+
if (message &&
|
|
3711
|
+
typeof message.content === "string" &&
|
|
3712
|
+
!message.refusal) {
|
|
3713
|
+
try {
|
|
3714
|
+
message.parsed = JSON.parse(message.content);
|
|
3715
|
+
}
|
|
3716
|
+
catch {
|
|
3717
|
+
message.parsed = null;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
return json;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
//
|
|
3728
|
+
//
|
|
3729
|
+
// Main
|
|
3730
|
+
//
|
|
3731
|
+
/**
|
|
3732
|
+
* Local replacement for `openai/helpers/zod`'s `zodResponseFormat`. Builds the
|
|
3733
|
+
* `response_format` payload OpenAI's Chat Completions / Responses APIs expect
|
|
3734
|
+
* from a Zod schema. The SDK always emits `strict: true`; callers re-derive or
|
|
3735
|
+
* sanitize `schema` as needed (e.g. forcing `additionalProperties: false`).
|
|
3736
|
+
*/
|
|
3737
|
+
function zodResponseFormat(schema, name) {
|
|
3738
|
+
return {
|
|
3739
|
+
type: "json_schema",
|
|
3740
|
+
json_schema: {
|
|
3741
|
+
name,
|
|
3742
|
+
strict: true,
|
|
3743
|
+
schema: v4.z.toJSONSchema(schema),
|
|
3744
|
+
},
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3511
3748
|
// Patterns for OpenAI reasoning models that support extended thinking
|
|
3512
3749
|
const REASONING_MODEL_PATTERNS = [
|
|
3513
3750
|
/^gpt-[5-9]/, // GPT-5 and above (gpt-5, gpt-5.1, gpt-5.2, gpt-6, etc.)
|
|
@@ -3524,19 +3761,19 @@ function isReasoningModel(model) {
|
|
|
3524
3761
|
// Constants
|
|
3525
3762
|
//
|
|
3526
3763
|
const RETRYABLE_ERROR_TYPES = [
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3764
|
+
APIConnectionError,
|
|
3765
|
+
APIConnectionTimeoutError,
|
|
3766
|
+
InternalServerError$1,
|
|
3530
3767
|
];
|
|
3531
3768
|
const NOT_RETRYABLE_ERROR_TYPES = [
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3769
|
+
APIUserAbortError,
|
|
3770
|
+
AuthenticationError$1,
|
|
3771
|
+
BadRequestError$1,
|
|
3772
|
+
ConflictError,
|
|
3773
|
+
NotFoundError$1,
|
|
3774
|
+
PermissionDeniedError$1,
|
|
3775
|
+
RateLimitError$1,
|
|
3776
|
+
UnprocessableEntityError,
|
|
3540
3777
|
];
|
|
3541
3778
|
// Models known not to accept `temperature`.
|
|
3542
3779
|
// Patterns (not exact names) so dated variants and future releases are covered
|
|
@@ -3549,7 +3786,7 @@ const MODELS_WITHOUT_TEMPERATURE$1 = [
|
|
|
3549
3786
|
function isTemperatureDeprecationError$1(error) {
|
|
3550
3787
|
if (!error || typeof error !== "object")
|
|
3551
3788
|
return false;
|
|
3552
|
-
if (!(error instanceof
|
|
3789
|
+
if (!(error instanceof BadRequestError$1) &&
|
|
3553
3790
|
error.status !== 400) {
|
|
3554
3791
|
return false;
|
|
3555
3792
|
}
|
|
@@ -3656,7 +3893,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
3656
3893
|
const zodSchema = schema instanceof v4.z.ZodType
|
|
3657
3894
|
? schema
|
|
3658
3895
|
: naturalZodSchema(schema);
|
|
3659
|
-
const responseFormat =
|
|
3896
|
+
const responseFormat = zodResponseFormat(zodSchema, "response");
|
|
3660
3897
|
// Re-spread to drop zod v4's non-enumerable `~standard` Standard-Schema
|
|
3661
3898
|
// interop marker, then strip `$schema`: OpenAI's strict json_schema subset
|
|
3662
3899
|
// does not recognize the draft-2020-12 `$schema` keyword, and shipping it
|
|
@@ -3903,7 +4140,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
3903
4140
|
//
|
|
3904
4141
|
classifyError(error) {
|
|
3905
4142
|
// Check for rate limit error
|
|
3906
|
-
if (error instanceof
|
|
4143
|
+
if (error instanceof RateLimitError$1) {
|
|
3907
4144
|
return {
|
|
3908
4145
|
error,
|
|
3909
4146
|
category: ErrorCategory.RateLimit,
|
|
@@ -4121,7 +4358,49 @@ function convertContentToOpenRouter(content) {
|
|
|
4121
4358
|
}
|
|
4122
4359
|
return parts;
|
|
4123
4360
|
}
|
|
4124
|
-
|
|
4361
|
+
/**
|
|
4362
|
+
* Convert internal content parts to the OpenAI-compatible wire shape. The
|
|
4363
|
+
* internal representation uses camelCase keys (`imageUrl`, `fileData`) that the
|
|
4364
|
+
* SDK accepted; the REST API wants snake_case (`image_url`, `file_data`).
|
|
4365
|
+
*/
|
|
4366
|
+
function contentToWire(content) {
|
|
4367
|
+
if (content === null ||
|
|
4368
|
+
content === undefined ||
|
|
4369
|
+
typeof content === "string") {
|
|
4370
|
+
return content;
|
|
4371
|
+
}
|
|
4372
|
+
return content.map((part) => {
|
|
4373
|
+
if (part.type === "image_url") {
|
|
4374
|
+
return { type: "image_url", image_url: part.imageUrl };
|
|
4375
|
+
}
|
|
4376
|
+
if (part.type === "file") {
|
|
4377
|
+
return {
|
|
4378
|
+
type: "file",
|
|
4379
|
+
file: { filename: part.file.filename, file_data: part.file.fileData },
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
return part;
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Serialize an internal message to the OpenAI-compatible wire shape, mapping
|
|
4387
|
+
* camelCase tool fields (`toolCalls`, `toolCallId`) to snake_case. Tool-call
|
|
4388
|
+
* objects are already wire-shaped (`{ id, type, function: { name, arguments } }`).
|
|
4389
|
+
*/
|
|
4390
|
+
function messageToWire(message) {
|
|
4391
|
+
const wire = { role: message.role };
|
|
4392
|
+
if (message.content !== undefined) {
|
|
4393
|
+
wire.content = contentToWire(message.content);
|
|
4394
|
+
}
|
|
4395
|
+
if (message.toolCalls) {
|
|
4396
|
+
wire.tool_calls = message.toolCalls;
|
|
4397
|
+
}
|
|
4398
|
+
if (message.toolCallId) {
|
|
4399
|
+
wire.tool_call_id = message.toolCallId;
|
|
4400
|
+
}
|
|
4401
|
+
return wire;
|
|
4402
|
+
}
|
|
4403
|
+
// OpenRouter error types based on HTTP status codes
|
|
4125
4404
|
const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
|
|
4126
4405
|
const RATE_LIMIT_STATUS_CODE = 429;
|
|
4127
4406
|
/**
|
|
@@ -4324,7 +4603,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4324
4603
|
const openRouterRequest = request;
|
|
4325
4604
|
const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
|
|
4326
4605
|
try {
|
|
4327
|
-
const response = (await openRouter.
|
|
4606
|
+
const response = (await openRouter.chatCompletion(this.toWireBody(openRouterRequest), signal ? { signal } : undefined));
|
|
4328
4607
|
if (wantsStructuredOutput) {
|
|
4329
4608
|
response.__jaypieStructuredOutput = true;
|
|
4330
4609
|
}
|
|
@@ -4341,7 +4620,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4341
4620
|
this.rememberModelRejectsTemperature(openRouterRequest.model);
|
|
4342
4621
|
const retryRequest = { ...openRouterRequest };
|
|
4343
4622
|
delete retryRequest.temperature;
|
|
4344
|
-
const response = (await openRouter.
|
|
4623
|
+
const response = (await openRouter.chatCompletion(this.toWireBody(retryRequest), signal ? { signal } : undefined));
|
|
4345
4624
|
if (wantsStructuredOutput) {
|
|
4346
4625
|
response.__jaypieStructuredOutput = true;
|
|
4347
4626
|
}
|
|
@@ -4354,7 +4633,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4354
4633
|
this.rememberModelRejectsStructuredOutput(model);
|
|
4355
4634
|
log$1.log.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
|
|
4356
4635
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
|
|
4357
|
-
return (await openRouter.
|
|
4636
|
+
return (await openRouter.chatCompletion(this.toWireBody(fallbackRequest), signal ? { signal } : undefined));
|
|
4358
4637
|
}
|
|
4359
4638
|
throw error;
|
|
4360
4639
|
}
|
|
@@ -4364,31 +4643,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4364
4643
|
* camelCase shape, forwarding only the fields we care about (the SDK
|
|
4365
4644
|
* silently strips unknown fields).
|
|
4366
4645
|
*/
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4646
|
+
/**
|
|
4647
|
+
* Serialize the internal request into the OpenAI-compatible wire body for
|
|
4648
|
+
* OpenRouter's Chat Completions endpoint. Top-level fields (model, tools,
|
|
4649
|
+
* tool_choice, response_format, user, temperature, and any providerOptions)
|
|
4650
|
+
* are already wire-shaped (snake_case); only messages carry camelCase tool
|
|
4651
|
+
* fields that must become snake_case on the wire.
|
|
4652
|
+
*/
|
|
4653
|
+
toWireBody(openRouterRequest) {
|
|
4654
|
+
return {
|
|
4655
|
+
...openRouterRequest,
|
|
4656
|
+
messages: openRouterRequest.messages.map(messageToWire),
|
|
4374
4657
|
};
|
|
4375
|
-
if (openRouterRequest.response_format) {
|
|
4376
|
-
const format = openRouterRequest.response_format;
|
|
4377
|
-
if (format.type === "json_schema") {
|
|
4378
|
-
params.responseFormat = {
|
|
4379
|
-
type: "json_schema",
|
|
4380
|
-
jsonSchema: format.json_schema,
|
|
4381
|
-
};
|
|
4382
|
-
}
|
|
4383
|
-
else {
|
|
4384
|
-
params.responseFormat = format;
|
|
4385
|
-
}
|
|
4386
|
-
}
|
|
4387
|
-
const temperature = openRouterRequest.temperature;
|
|
4388
|
-
if (temperature !== undefined) {
|
|
4389
|
-
params.temperature = temperature;
|
|
4390
|
-
}
|
|
4391
|
-
return params;
|
|
4392
4658
|
}
|
|
4393
4659
|
/**
|
|
4394
4660
|
* Rebuild a structured-output request without `response_format`, swapping in
|
|
@@ -4420,15 +4686,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4420
4686
|
async *executeStreamRequest(client, request, signal) {
|
|
4421
4687
|
const openRouter = client;
|
|
4422
4688
|
const openRouterRequest = request;
|
|
4423
|
-
//
|
|
4424
|
-
//
|
|
4425
|
-
|
|
4426
|
-
// non-stream response.
|
|
4427
|
-
const streamParams = {
|
|
4428
|
-
...this.toSdkChatParams(openRouterRequest),
|
|
4429
|
-
stream: true,
|
|
4430
|
-
};
|
|
4431
|
-
const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
|
|
4689
|
+
// streamChatCompletion adds `stream: true` + `stream_options.include_usage`
|
|
4690
|
+
// and yields decoded SSE chunks in OpenAI-compatible (snake_case) shape.
|
|
4691
|
+
const stream = openRouter.streamChatCompletion(this.toWireBody(openRouterRequest), signal ? { signal } : undefined);
|
|
4432
4692
|
// Track current tool call being built
|
|
4433
4693
|
let currentToolCall = null;
|
|
4434
4694
|
// Track usage for final chunk
|
|
@@ -4877,7 +5137,7 @@ const TRANSIENT_INGEST_MESSAGE_PATTERNS = [
|
|
|
4877
5137
|
"failed to ingest inline file bytes",
|
|
4878
5138
|
];
|
|
4879
5139
|
function isTransientIngestError(error) {
|
|
4880
|
-
if (!(error instanceof
|
|
5140
|
+
if (!(error instanceof BadRequestError$1))
|
|
4881
5141
|
return false;
|
|
4882
5142
|
const message = (error.message ?? "").toLowerCase();
|
|
4883
5143
|
return TRANSIENT_INGEST_MESSAGE_PATTERNS.some((pattern) => message.includes(pattern));
|
|
@@ -5069,7 +5329,7 @@ const requireModule = typeof __filename !== "undefined"
|
|
|
5069
5329
|
? module$1.createRequire(url.pathToFileURL(__filename).href)
|
|
5070
5330
|
: module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
5071
5331
|
let resolved = false;
|
|
5072
|
-
let cachedSdk$
|
|
5332
|
+
let cachedSdk$1 = null;
|
|
5073
5333
|
/**
|
|
5074
5334
|
* Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
|
|
5075
5335
|
* when dd-trace is absent or the SDK surface is unexpected. Cached after the
|
|
@@ -5077,7 +5337,7 @@ let cachedSdk$4 = null;
|
|
|
5077
5337
|
*/
|
|
5078
5338
|
function resolveLlmObs() {
|
|
5079
5339
|
if (resolved) {
|
|
5080
|
-
return cachedSdk$
|
|
5340
|
+
return cachedSdk$1;
|
|
5081
5341
|
}
|
|
5082
5342
|
resolved = true;
|
|
5083
5343
|
try {
|
|
@@ -5087,13 +5347,13 @@ function resolveLlmObs() {
|
|
|
5087
5347
|
if (llmobs &&
|
|
5088
5348
|
typeof llmobs.trace === "function" &&
|
|
5089
5349
|
typeof llmobs.annotate === "function") {
|
|
5090
|
-
cachedSdk$
|
|
5350
|
+
cachedSdk$1 = llmobs;
|
|
5091
5351
|
}
|
|
5092
5352
|
}
|
|
5093
5353
|
catch {
|
|
5094
|
-
cachedSdk$
|
|
5354
|
+
cachedSdk$1 = null;
|
|
5095
5355
|
}
|
|
5096
|
-
return cachedSdk$
|
|
5356
|
+
return cachedSdk$1;
|
|
5097
5357
|
}
|
|
5098
5358
|
//
|
|
5099
5359
|
//
|
|
@@ -7007,19 +7267,125 @@ function createStreamLoop(config) {
|
|
|
7007
7267
|
return new StreamLoop(config);
|
|
7008
7268
|
}
|
|
7009
7269
|
|
|
7010
|
-
//
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7270
|
+
//
|
|
7271
|
+
//
|
|
7272
|
+
// Constants
|
|
7273
|
+
//
|
|
7274
|
+
const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
7275
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
7276
|
+
//
|
|
7277
|
+
//
|
|
7278
|
+
// Errors
|
|
7279
|
+
//
|
|
7280
|
+
// The adapter's `classifyError` keys off `error.constructor.name` (the SDK's
|
|
7281
|
+
// class names), so the client throws errors whose class names match. Status →
|
|
7282
|
+
// class mapping mirrors the SDK.
|
|
7283
|
+
//
|
|
7284
|
+
class AnthropicApiError extends Error {
|
|
7285
|
+
constructor(status, message, error) {
|
|
7286
|
+
super(message);
|
|
7287
|
+
this.name = this.constructor.name;
|
|
7288
|
+
this.status = status;
|
|
7289
|
+
this.error = error;
|
|
7018
7290
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7291
|
+
}
|
|
7292
|
+
class BadRequestError extends AnthropicApiError {
|
|
7293
|
+
}
|
|
7294
|
+
class AuthenticationError extends AnthropicApiError {
|
|
7295
|
+
}
|
|
7296
|
+
class PermissionDeniedError extends AnthropicApiError {
|
|
7297
|
+
}
|
|
7298
|
+
class NotFoundError extends AnthropicApiError {
|
|
7299
|
+
}
|
|
7300
|
+
class RateLimitError extends AnthropicApiError {
|
|
7301
|
+
}
|
|
7302
|
+
class InternalServerError extends AnthropicApiError {
|
|
7303
|
+
}
|
|
7304
|
+
function errorForStatus(status, message, error) {
|
|
7305
|
+
if (status === 400)
|
|
7306
|
+
return new BadRequestError(status, message, error);
|
|
7307
|
+
if (status === 401)
|
|
7308
|
+
return new AuthenticationError(status, message, error);
|
|
7309
|
+
if (status === 403)
|
|
7310
|
+
return new PermissionDeniedError(status, message, error);
|
|
7311
|
+
if (status === 404)
|
|
7312
|
+
return new NotFoundError(status, message, error);
|
|
7313
|
+
if (status === 429)
|
|
7314
|
+
return new RateLimitError(status, message, error);
|
|
7315
|
+
if (status >= 500)
|
|
7316
|
+
return new InternalServerError(status, message, error);
|
|
7317
|
+
return new AnthropicApiError(status, message, error);
|
|
7318
|
+
}
|
|
7319
|
+
//
|
|
7320
|
+
//
|
|
7321
|
+
// Main
|
|
7322
|
+
//
|
|
7323
|
+
/**
|
|
7324
|
+
* Minimal `fetch`-based client for Anthropic's Messages API. Replaces
|
|
7325
|
+
* `@anthropic-ai/sdk` — the adapter only needs `messages.create` (streaming and
|
|
7326
|
+
* non-streaming), header auth, and HTTP errors whose class names drive
|
|
7327
|
+
* `classifyError`. Internal request params are already Anthropic's wire shape,
|
|
7328
|
+
* so they serialize verbatim. Exposes a `messages` facade mirroring the SDK so
|
|
7329
|
+
* the adapter call sites are unchanged.
|
|
7330
|
+
*/
|
|
7331
|
+
class AnthropicClient {
|
|
7332
|
+
constructor({ apiKey, baseURL = ANTHROPIC_BASE_URL, }) {
|
|
7333
|
+
this.apiKey = apiKey;
|
|
7334
|
+
this.baseURL = baseURL;
|
|
7335
|
+
this.messages = {
|
|
7336
|
+
create: ((params, options) => params.stream
|
|
7337
|
+
? this.createStream(params, options)
|
|
7338
|
+
: this.createMessage(params, options)),
|
|
7339
|
+
};
|
|
7340
|
+
}
|
|
7341
|
+
headers() {
|
|
7342
|
+
return {
|
|
7343
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
7344
|
+
"content-type": "application/json",
|
|
7345
|
+
"x-api-key": this.apiKey,
|
|
7346
|
+
};
|
|
7347
|
+
}
|
|
7348
|
+
async toError(response) {
|
|
7349
|
+
let message = `Anthropic request failed with status ${response.status}`;
|
|
7350
|
+
let error;
|
|
7351
|
+
try {
|
|
7352
|
+
const body = (await response.json());
|
|
7353
|
+
if (body?.error?.message) {
|
|
7354
|
+
message = body.error.message;
|
|
7355
|
+
error = body.error;
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
catch {
|
|
7359
|
+
// Non-JSON error body; keep the status-based message.
|
|
7360
|
+
}
|
|
7361
|
+
return errorForStatus(response.status, message, error);
|
|
7362
|
+
}
|
|
7363
|
+
async createMessage(params, { signal } = {}) {
|
|
7364
|
+
const response = await fetch(`${this.baseURL}/messages`, {
|
|
7365
|
+
method: "POST",
|
|
7366
|
+
headers: this.headers(),
|
|
7367
|
+
body: JSON.stringify(params),
|
|
7368
|
+
signal,
|
|
7369
|
+
});
|
|
7370
|
+
if (!response.ok)
|
|
7371
|
+
throw await this.toError(response);
|
|
7372
|
+
return (await response.json());
|
|
7373
|
+
}
|
|
7374
|
+
async createStream(params, { signal } = {}) {
|
|
7375
|
+
const response = await fetch(`${this.baseURL}/messages`, {
|
|
7376
|
+
method: "POST",
|
|
7377
|
+
headers: { ...this.headers(), accept: "text/event-stream" },
|
|
7378
|
+
body: JSON.stringify(params),
|
|
7379
|
+
signal,
|
|
7380
|
+
});
|
|
7381
|
+
if (!response.ok)
|
|
7382
|
+
throw await this.toError(response);
|
|
7383
|
+
if (!response.body)
|
|
7384
|
+
return (async function* () { })();
|
|
7385
|
+
return parseSseStream(response.body);
|
|
7021
7386
|
}
|
|
7022
7387
|
}
|
|
7388
|
+
|
|
7023
7389
|
// Logger
|
|
7024
7390
|
const getLogger$5 = () => log$1.log.lib({ lib: kit.JAYPIE.LIB.LLM });
|
|
7025
7391
|
// Client initialization
|
|
@@ -7029,8 +7395,7 @@ async function initializeClient$5({ apiKey, } = {}) {
|
|
|
7029
7395
|
if (!resolvedApiKey) {
|
|
7030
7396
|
throw new errors.ConfigurationError("The application could not resolve the required API key: ANTHROPIC_API_KEY");
|
|
7031
7397
|
}
|
|
7032
|
-
const
|
|
7033
|
-
const client = new sdk.default({ apiKey: resolvedApiKey });
|
|
7398
|
+
const client = new AnthropicClient({ apiKey: resolvedApiKey });
|
|
7034
7399
|
logger.trace("Initialized Anthropic client");
|
|
7035
7400
|
return client;
|
|
7036
7401
|
}
|
|
@@ -7130,6 +7495,7 @@ async function createStructuredCompletion$1(client, messages, model, responseSch
|
|
|
7130
7495
|
[exports.LlmMessageRole.Assistant]: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
7131
7496
|
[exports.LlmMessageRole.Developer]: PROVIDER.ANTHROPIC.ROLE.SYSTEM,
|
|
7132
7497
|
});
|
|
7498
|
+
/* eslint-enable @typescript-eslint/no-namespace */
|
|
7133
7499
|
|
|
7134
7500
|
// Main class implementation
|
|
7135
7501
|
class AnthropicProvider {
|
|
@@ -7218,13 +7584,13 @@ class AnthropicProvider {
|
|
|
7218
7584
|
}
|
|
7219
7585
|
}
|
|
7220
7586
|
|
|
7221
|
-
let cachedSdk
|
|
7222
|
-
async function loadSdk
|
|
7223
|
-
if (cachedSdk
|
|
7224
|
-
return cachedSdk
|
|
7587
|
+
let cachedSdk = null;
|
|
7588
|
+
async function loadSdk() {
|
|
7589
|
+
if (cachedSdk)
|
|
7590
|
+
return cachedSdk;
|
|
7225
7591
|
try {
|
|
7226
|
-
cachedSdk
|
|
7227
|
-
return cachedSdk
|
|
7592
|
+
cachedSdk = await import('@aws-sdk/client-bedrock-runtime');
|
|
7593
|
+
return cachedSdk;
|
|
7228
7594
|
}
|
|
7229
7595
|
catch {
|
|
7230
7596
|
throw new errors.ConfigurationError("@aws-sdk/client-bedrock-runtime is required but not installed. Run: npm install @aws-sdk/client-bedrock-runtime");
|
|
@@ -7234,7 +7600,7 @@ const getLogger$4 = () => log$1.log.lib({ lib: kit.JAYPIE.LIB.LLM });
|
|
|
7234
7600
|
async function initializeClient$4({ region, } = {}) {
|
|
7235
7601
|
const logger = getLogger$4();
|
|
7236
7602
|
const resolvedRegion = region || process.env.AWS_REGION || "us-east-1";
|
|
7237
|
-
const sdk = await loadSdk
|
|
7603
|
+
const sdk = await loadSdk();
|
|
7238
7604
|
const client = new sdk.BedrockRuntimeClient({ region: resolvedRegion });
|
|
7239
7605
|
logger.trace("Initialized Bedrock client");
|
|
7240
7606
|
return client;
|
|
@@ -7299,19 +7665,156 @@ class BedrockProvider {
|
|
|
7299
7665
|
}
|
|
7300
7666
|
}
|
|
7301
7667
|
|
|
7302
|
-
//
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7668
|
+
//
|
|
7669
|
+
//
|
|
7670
|
+
// Constants
|
|
7671
|
+
//
|
|
7672
|
+
const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
7673
|
+
// Config keys the REST API expects at the top level of the request body. Every
|
|
7674
|
+
// other key in the SDK-style `config` object belongs under `generationConfig`.
|
|
7675
|
+
const TOP_LEVEL_CONFIG_KEYS = new Set([
|
|
7676
|
+
"systemInstruction",
|
|
7677
|
+
"tools",
|
|
7678
|
+
"toolConfig",
|
|
7679
|
+
"safetySettings",
|
|
7680
|
+
"cachedContent",
|
|
7681
|
+
]);
|
|
7682
|
+
/**
|
|
7683
|
+
* HTTP error carrying the upstream status and parsed API message. The
|
|
7684
|
+
* GoogleAdapter classifies errors by reading `.status` / `.code` and
|
|
7685
|
+
* `.message`, so this shape keeps `classifyError` working unchanged after
|
|
7686
|
+
* dropping the SDK.
|
|
7687
|
+
*/
|
|
7688
|
+
class GoogleHttpError extends Error {
|
|
7689
|
+
constructor(status, message) {
|
|
7690
|
+
super(message);
|
|
7691
|
+
this.name = "GoogleHttpError";
|
|
7692
|
+
this.status = status;
|
|
7693
|
+
this.code = status;
|
|
7310
7694
|
}
|
|
7311
|
-
|
|
7312
|
-
|
|
7695
|
+
}
|
|
7696
|
+
//
|
|
7697
|
+
//
|
|
7698
|
+
// Helpers
|
|
7699
|
+
//
|
|
7700
|
+
/**
|
|
7701
|
+
* Translate the SDK-style `{ model, contents, config }` request into the REST
|
|
7702
|
+
* `generateContent` body. `systemInstruction` (a string) becomes a Content;
|
|
7703
|
+
* top-level config keys pass through; all remaining config keys (temperature,
|
|
7704
|
+
* responseMimeType, responseSchema, responseJsonSchema, ...) move under
|
|
7705
|
+
* `generationConfig`.
|
|
7706
|
+
*/
|
|
7707
|
+
function toRestBody(params) {
|
|
7708
|
+
const body = {
|
|
7709
|
+
contents: params.contents,
|
|
7710
|
+
};
|
|
7711
|
+
const config = params.config;
|
|
7712
|
+
if (!config)
|
|
7713
|
+
return body;
|
|
7714
|
+
const generationConfig = {};
|
|
7715
|
+
for (const [key, value] of Object.entries(config)) {
|
|
7716
|
+
if (value === undefined)
|
|
7717
|
+
continue;
|
|
7718
|
+
if (key === "systemInstruction") {
|
|
7719
|
+
body.systemInstruction =
|
|
7720
|
+
typeof value === "string"
|
|
7721
|
+
? { parts: [{ text: value }] }
|
|
7722
|
+
: value;
|
|
7723
|
+
}
|
|
7724
|
+
else if (TOP_LEVEL_CONFIG_KEYS.has(key)) {
|
|
7725
|
+
body[key] = value;
|
|
7726
|
+
}
|
|
7727
|
+
else {
|
|
7728
|
+
generationConfig[key] = value;
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
if (Object.keys(generationConfig).length > 0) {
|
|
7732
|
+
body.generationConfig = generationConfig;
|
|
7313
7733
|
}
|
|
7734
|
+
return body;
|
|
7735
|
+
}
|
|
7736
|
+
/**
|
|
7737
|
+
* Concatenate the non-thought text parts of the first candidate, matching the
|
|
7738
|
+
* SDK's `response.text` convenience getter (consumed by `GoogleProvider.send`).
|
|
7739
|
+
*/
|
|
7740
|
+
function responseText(response) {
|
|
7741
|
+
const parts = response.candidates?.[0]?.content?.parts;
|
|
7742
|
+
if (!parts)
|
|
7743
|
+
return undefined;
|
|
7744
|
+
const text = parts
|
|
7745
|
+
.filter((part) => part.text && !part.thought)
|
|
7746
|
+
.map((part) => part.text)
|
|
7747
|
+
.join("");
|
|
7748
|
+
return text.length > 0 ? text : undefined;
|
|
7314
7749
|
}
|
|
7750
|
+
//
|
|
7751
|
+
//
|
|
7752
|
+
// Main
|
|
7753
|
+
//
|
|
7754
|
+
/**
|
|
7755
|
+
* Minimal `fetch`-based client for Google's Generative Language (Gemini) REST
|
|
7756
|
+
* API. Replaces `@google/genai` — the adapter only needs `generateContent`
|
|
7757
|
+
* (streaming and non-streaming), an api-key header, and HTTP error surfacing.
|
|
7758
|
+
* Exposes a `models` facade mirroring the SDK so the adapter call sites are
|
|
7759
|
+
* unchanged.
|
|
7760
|
+
*/
|
|
7761
|
+
class GoogleClient {
|
|
7762
|
+
constructor({ apiKey, baseURL = GOOGLE_BASE_URL }) {
|
|
7763
|
+
this.apiKey = apiKey;
|
|
7764
|
+
this.baseURL = baseURL;
|
|
7765
|
+
this.models = {
|
|
7766
|
+
generateContent: (params, options) => this.generateContent(params, options),
|
|
7767
|
+
generateContentStream: (params, options) => this.generateContentStream(params, options),
|
|
7768
|
+
};
|
|
7769
|
+
}
|
|
7770
|
+
headers() {
|
|
7771
|
+
return {
|
|
7772
|
+
"Content-Type": "application/json",
|
|
7773
|
+
"x-goog-api-key": this.apiKey,
|
|
7774
|
+
};
|
|
7775
|
+
}
|
|
7776
|
+
async toError(response) {
|
|
7777
|
+
let message = `Google request failed with status ${response.status}`;
|
|
7778
|
+
try {
|
|
7779
|
+
const body = (await response.json());
|
|
7780
|
+
if (body?.error?.message)
|
|
7781
|
+
message = body.error.message;
|
|
7782
|
+
}
|
|
7783
|
+
catch {
|
|
7784
|
+
// Non-JSON error body; keep the status-based message.
|
|
7785
|
+
}
|
|
7786
|
+
return new GoogleHttpError(response.status, message);
|
|
7787
|
+
}
|
|
7788
|
+
async generateContent(params, { signal } = {}) {
|
|
7789
|
+
const url = `${this.baseURL}/models/${params.model}:generateContent`;
|
|
7790
|
+
const response = await fetch(url, {
|
|
7791
|
+
method: "POST",
|
|
7792
|
+
headers: this.headers(),
|
|
7793
|
+
body: JSON.stringify(toRestBody(params)),
|
|
7794
|
+
signal,
|
|
7795
|
+
});
|
|
7796
|
+
if (!response.ok)
|
|
7797
|
+
throw await this.toError(response);
|
|
7798
|
+
const json = (await response.json());
|
|
7799
|
+
json.text = responseText(json);
|
|
7800
|
+
return json;
|
|
7801
|
+
}
|
|
7802
|
+
async generateContentStream(params, { signal } = {}) {
|
|
7803
|
+
const url = `${this.baseURL}/models/${params.model}:streamGenerateContent?alt=sse`;
|
|
7804
|
+
const response = await fetch(url, {
|
|
7805
|
+
method: "POST",
|
|
7806
|
+
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
7807
|
+
body: JSON.stringify(toRestBody(params)),
|
|
7808
|
+
signal,
|
|
7809
|
+
});
|
|
7810
|
+
if (!response.ok)
|
|
7811
|
+
throw await this.toError(response);
|
|
7812
|
+
if (!response.body)
|
|
7813
|
+
return (async function* () { })();
|
|
7814
|
+
return parseSseStream(response.body);
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7315
7818
|
// Logger
|
|
7316
7819
|
const getLogger$3 = () => log$1.log.lib({ lib: kit.JAYPIE.LIB.LLM });
|
|
7317
7820
|
// Client initialization
|
|
@@ -7321,8 +7824,7 @@ async function initializeClient$3({ apiKey, } = {}) {
|
|
|
7321
7824
|
if (!resolvedApiKey) {
|
|
7322
7825
|
throw new errors.ConfigurationError("The application could not resolve the requested keys");
|
|
7323
7826
|
}
|
|
7324
|
-
const
|
|
7325
|
-
const client = new sdk.GoogleGenAI({ apiKey: resolvedApiKey });
|
|
7827
|
+
const client = new GoogleClient({ apiKey: resolvedApiKey });
|
|
7326
7828
|
logger.trace("Initialized Gemini client");
|
|
7327
7829
|
return client;
|
|
7328
7830
|
}
|
|
@@ -7471,7 +7973,7 @@ async function initializeClient$2({ apiKey, } = {}) {
|
|
|
7471
7973
|
if (!resolvedApiKey) {
|
|
7472
7974
|
throw new errors.ConfigurationError("The application could not resolve the requested keys");
|
|
7473
7975
|
}
|
|
7474
|
-
const client = new
|
|
7976
|
+
const client = new OpenAIClient({ apiKey: resolvedApiKey });
|
|
7475
7977
|
logger.trace("Initialized OpenAI client");
|
|
7476
7978
|
return client;
|
|
7477
7979
|
}
|
|
@@ -7513,7 +8015,7 @@ async function createStructuredCompletion(client, { messages, responseSchema, mo
|
|
|
7513
8015
|
const zodSchema = responseSchema instanceof v4.z.ZodType
|
|
7514
8016
|
? responseSchema
|
|
7515
8017
|
: naturalZodSchema(responseSchema);
|
|
7516
|
-
const responseFormat =
|
|
8018
|
+
const responseFormat = zodResponseFormat(zodSchema, "response");
|
|
7517
8019
|
const jsonSchema = v4.z.toJSONSchema(zodSchema);
|
|
7518
8020
|
// Temporary hack because OpenAI requires additional_properties to be false on all objects
|
|
7519
8021
|
const checks = [jsonSchema];
|
|
@@ -7530,23 +8032,23 @@ async function createStructuredCompletion(client, { messages, responseSchema, mo
|
|
|
7530
8032
|
checks.shift();
|
|
7531
8033
|
}
|
|
7532
8034
|
responseFormat.json_schema.schema = jsonSchema;
|
|
7533
|
-
const completion = await client.chat.completions.parse({
|
|
8035
|
+
const completion = (await client.chat.completions.parse({
|
|
7534
8036
|
messages,
|
|
7535
8037
|
model,
|
|
7536
8038
|
response_format: responseFormat,
|
|
7537
|
-
});
|
|
8039
|
+
}));
|
|
7538
8040
|
logger.var({ assistantReply: completion.choices[0].message.parsed });
|
|
7539
8041
|
return completion.choices[0].message.parsed;
|
|
7540
8042
|
}
|
|
7541
8043
|
async function createTextCompletion(client, { messages, model, }) {
|
|
7542
8044
|
const logger = getLogger$2();
|
|
7543
8045
|
logger.trace("Using text output (unstructured)");
|
|
7544
|
-
const completion = await client.chat.completions.create({
|
|
8046
|
+
const completion = (await client.chat.completions.create({
|
|
7545
8047
|
messages,
|
|
7546
8048
|
model,
|
|
7547
|
-
});
|
|
7548
|
-
logger.trace(`Assistant reply: ${completion.choices[0]?.message?.content?.length} characters`);
|
|
7549
|
-
return completion.choices[0]?.message?.content || "";
|
|
8049
|
+
}));
|
|
8050
|
+
logger.trace(`Assistant reply: ${completion.choices?.[0]?.message?.content?.length} characters`);
|
|
8051
|
+
return completion.choices?.[0]?.message?.content || "";
|
|
7550
8052
|
}
|
|
7551
8053
|
|
|
7552
8054
|
class OpenAiProvider {
|
|
@@ -7633,19 +8135,139 @@ class OpenAiProvider {
|
|
|
7633
8135
|
}
|
|
7634
8136
|
}
|
|
7635
8137
|
|
|
7636
|
-
//
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
8138
|
+
//
|
|
8139
|
+
//
|
|
8140
|
+
// Constants
|
|
8141
|
+
//
|
|
8142
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
8143
|
+
/**
|
|
8144
|
+
* HTTP error carrying the upstream status and parsed API message. The
|
|
8145
|
+
* OpenRouterAdapter classifies errors by reading `.status` / `.statusCode` and
|
|
8146
|
+
* `.message` / `.error.message`, so this shape keeps `classifyError`,
|
|
8147
|
+
* `isTemperatureDeprecationError`, and `isStructuredOutputUnsupportedError`
|
|
8148
|
+
* working unchanged after dropping the SDK.
|
|
8149
|
+
*/
|
|
8150
|
+
class OpenRouterHttpError extends Error {
|
|
8151
|
+
constructor(status, message, error) {
|
|
8152
|
+
super(message);
|
|
8153
|
+
this.name = "OpenRouterHttpError";
|
|
8154
|
+
this.status = status;
|
|
8155
|
+
this.statusCode = status;
|
|
8156
|
+
this.error = error;
|
|
7644
8157
|
}
|
|
7645
|
-
|
|
7646
|
-
|
|
8158
|
+
}
|
|
8159
|
+
//
|
|
8160
|
+
//
|
|
8161
|
+
// Helpers
|
|
8162
|
+
//
|
|
8163
|
+
/**
|
|
8164
|
+
* Normalize the snake_case wire response into the camelCase shape the adapter
|
|
8165
|
+
* readers expect (the SDK previously returned camelCase). Only protocol fields
|
|
8166
|
+
* are touched — user content (schema property names, tool argument JSON) is
|
|
8167
|
+
* left untouched.
|
|
8168
|
+
*/
|
|
8169
|
+
function normalizeResponse(json) {
|
|
8170
|
+
const choices = json.choices;
|
|
8171
|
+
if (Array.isArray(choices)) {
|
|
8172
|
+
for (const choice of choices) {
|
|
8173
|
+
if (choice.finish_reason !== undefined &&
|
|
8174
|
+
choice.finishReason === undefined) {
|
|
8175
|
+
choice.finishReason = choice.finish_reason;
|
|
8176
|
+
}
|
|
8177
|
+
const message = choice.message;
|
|
8178
|
+
if (message?.tool_calls !== undefined &&
|
|
8179
|
+
message.toolCalls === undefined) {
|
|
8180
|
+
message.toolCalls = message.tool_calls;
|
|
8181
|
+
}
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
8184
|
+
const usage = json.usage;
|
|
8185
|
+
if (usage) {
|
|
8186
|
+
if (usage.promptTokens === undefined)
|
|
8187
|
+
usage.promptTokens = usage.prompt_tokens;
|
|
8188
|
+
if (usage.completionTokens === undefined) {
|
|
8189
|
+
usage.completionTokens = usage.completion_tokens;
|
|
8190
|
+
}
|
|
8191
|
+
if (usage.totalTokens === undefined)
|
|
8192
|
+
usage.totalTokens = usage.total_tokens;
|
|
8193
|
+
const details = usage.completion_tokens_details;
|
|
8194
|
+
if (details?.reasoning_tokens !== undefined &&
|
|
8195
|
+
!usage.completionTokensDetails) {
|
|
8196
|
+
usage.completionTokensDetails = {
|
|
8197
|
+
reasoningTokens: details.reasoning_tokens,
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
7647
8200
|
}
|
|
8201
|
+
return json;
|
|
7648
8202
|
}
|
|
8203
|
+
//
|
|
8204
|
+
//
|
|
8205
|
+
// Main
|
|
8206
|
+
//
|
|
8207
|
+
/**
|
|
8208
|
+
* Minimal `fetch`-based client for OpenRouter's OpenAI-compatible Chat
|
|
8209
|
+
* Completions endpoint. Replaces `@openrouter/sdk` — the adapter only needs a
|
|
8210
|
+
* single POST (streaming and non-streaming), header auth, and HTTP error
|
|
8211
|
+
* surfacing.
|
|
8212
|
+
*/
|
|
8213
|
+
class OpenRouterClient {
|
|
8214
|
+
constructor({ apiKey, baseURL = OPENROUTER_BASE_URL, }) {
|
|
8215
|
+
this.apiKey = apiKey;
|
|
8216
|
+
this.baseURL = baseURL;
|
|
8217
|
+
}
|
|
8218
|
+
headers() {
|
|
8219
|
+
return {
|
|
8220
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
8221
|
+
"Content-Type": "application/json",
|
|
8222
|
+
};
|
|
8223
|
+
}
|
|
8224
|
+
async toError(response) {
|
|
8225
|
+
let message = `OpenRouter request failed with status ${response.status}`;
|
|
8226
|
+
let error;
|
|
8227
|
+
try {
|
|
8228
|
+
const body = (await response.json());
|
|
8229
|
+
if (body?.error?.message) {
|
|
8230
|
+
message = body.error.message;
|
|
8231
|
+
error = body.error;
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8234
|
+
catch {
|
|
8235
|
+
// Non-JSON error body; keep the status-based message.
|
|
8236
|
+
}
|
|
8237
|
+
return new OpenRouterHttpError(response.status, message, error);
|
|
8238
|
+
}
|
|
8239
|
+
async chatCompletion(body, { signal } = {}) {
|
|
8240
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
8241
|
+
method: "POST",
|
|
8242
|
+
headers: this.headers(),
|
|
8243
|
+
body: JSON.stringify(body),
|
|
8244
|
+
signal,
|
|
8245
|
+
});
|
|
8246
|
+
if (!response.ok)
|
|
8247
|
+
throw await this.toError(response);
|
|
8248
|
+
const json = (await response.json());
|
|
8249
|
+
return normalizeResponse(json);
|
|
8250
|
+
}
|
|
8251
|
+
async *streamChatCompletion(body, { signal } = {}) {
|
|
8252
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
8253
|
+
method: "POST",
|
|
8254
|
+
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
8255
|
+
// OpenAI-style streams only include usage when explicitly requested.
|
|
8256
|
+
body: JSON.stringify({
|
|
8257
|
+
...body,
|
|
8258
|
+
stream: true,
|
|
8259
|
+
stream_options: { include_usage: true },
|
|
8260
|
+
}),
|
|
8261
|
+
signal,
|
|
8262
|
+
});
|
|
8263
|
+
if (!response.ok)
|
|
8264
|
+
throw await this.toError(response);
|
|
8265
|
+
if (!response.body)
|
|
8266
|
+
return;
|
|
8267
|
+
yield* parseSseStream(response.body);
|
|
8268
|
+
}
|
|
8269
|
+
}
|
|
8270
|
+
|
|
7649
8271
|
// Logger
|
|
7650
8272
|
const getLogger$1 = () => log$1.log.lib({ lib: kit.JAYPIE.LIB.LLM });
|
|
7651
8273
|
// Client initialization
|
|
@@ -7655,8 +8277,7 @@ async function initializeClient$1({ apiKey, } = {}) {
|
|
|
7655
8277
|
if (!resolvedApiKey) {
|
|
7656
8278
|
throw new errors.ConfigurationError("The application could not resolve the requested keys");
|
|
7657
8279
|
}
|
|
7658
|
-
const
|
|
7659
|
-
const client = new sdk.OpenRouter({ apiKey: resolvedApiKey });
|
|
8280
|
+
const client = new OpenRouterClient({ apiKey: resolvedApiKey });
|
|
7660
8281
|
logger.trace("Initialized OpenRouter client");
|
|
7661
8282
|
return client;
|
|
7662
8283
|
}
|
|
@@ -7736,12 +8357,13 @@ class OpenRouterProvider {
|
|
|
7736
8357
|
const client = await this.getClient();
|
|
7737
8358
|
const messages = prepareMessages(message, options);
|
|
7738
8359
|
const modelToUse = options?.model || this.model;
|
|
7739
|
-
//
|
|
7740
|
-
const response = await client.
|
|
8360
|
+
// OpenAI-compatible Chat Completions body; messages are already wire-shaped.
|
|
8361
|
+
const response = await client.chatCompletion({
|
|
7741
8362
|
model: modelToUse,
|
|
7742
|
-
messages
|
|
8363
|
+
messages,
|
|
7743
8364
|
});
|
|
7744
|
-
const
|
|
8365
|
+
const choices = response.choices;
|
|
8366
|
+
const rawContent = choices?.[0]?.message?.content;
|
|
7745
8367
|
// Extract text content - content could be string or array of content items
|
|
7746
8368
|
const content = typeof rawContent === "string"
|
|
7747
8369
|
? rawContent
|
|
@@ -7804,7 +8426,7 @@ async function initializeClient({ apiKey, } = {}) {
|
|
|
7804
8426
|
if (!resolvedApiKey) {
|
|
7805
8427
|
throw new errors.ConfigurationError("The application could not resolve the requested keys");
|
|
7806
8428
|
}
|
|
7807
|
-
const client = new
|
|
8429
|
+
const client = new OpenAIClient({
|
|
7808
8430
|
apiKey: resolvedApiKey,
|
|
7809
8431
|
baseURL: PROVIDER.XAI.BASE_URL,
|
|
7810
8432
|
});
|