@sudobility/shapeshyft_engine 1.0.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.
- package/CLAUDE.md +27 -0
- package/README.md +11 -0
- package/dist/config/providers.d.ts +111 -0
- package/dist/config/providers.d.ts.map +1 -0
- package/dist/config/providers.js +1535 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-helper.d.ts +35 -0
- package/dist/lib/api-helper.d.ts.map +1 -0
- package/dist/lib/api-helper.js +123 -0
- package/dist/lib/api-helper.js.map +1 -0
- package/dist/lib/capability-validator.d.ts +36 -0
- package/dist/lib/capability-validator.d.ts.map +1 -0
- package/dist/lib/capability-validator.js +140 -0
- package/dist/lib/capability-validator.js.map +1 -0
- package/dist/lib/media-constants.d.ts +55 -0
- package/dist/lib/media-constants.d.ts.map +1 -0
- package/dist/lib/media-constants.js +166 -0
- package/dist/lib/media-constants.js.map +1 -0
- package/dist/lib/media-conversion.d.ts +36 -0
- package/dist/lib/media-conversion.d.ts.map +1 -0
- package/dist/lib/media-conversion.js +87 -0
- package/dist/lib/media-conversion.js.map +1 -0
- package/dist/lib/media-utils.d.ts +35 -0
- package/dist/lib/media-utils.d.ts.map +1 -0
- package/dist/lib/media-utils.js +173 -0
- package/dist/lib/media-utils.js.map +1 -0
- package/dist/lib/output-limit.d.ts +38 -0
- package/dist/lib/output-limit.d.ts.map +1 -0
- package/dist/lib/output-limit.js +54 -0
- package/dist/lib/output-limit.js.map +1 -0
- package/dist/lib/prompt-builder.d.ts +94 -0
- package/dist/lib/prompt-builder.d.ts.map +1 -0
- package/dist/lib/prompt-builder.js +408 -0
- package/dist/lib/prompt-builder.js.map +1 -0
- package/dist/lib/reserved-fields.d.ts +41 -0
- package/dist/lib/reserved-fields.d.ts.map +1 -0
- package/dist/lib/reserved-fields.js +47 -0
- package/dist/lib/reserved-fields.js.map +1 -0
- package/dist/services/llm/anthropic.d.ts +16 -0
- package/dist/services/llm/anthropic.d.ts.map +1 -0
- package/dist/services/llm/anthropic.js +154 -0
- package/dist/services/llm/anthropic.js.map +1 -0
- package/dist/services/llm/custom.d.ts +51 -0
- package/dist/services/llm/custom.d.ts.map +1 -0
- package/dist/services/llm/custom.js +419 -0
- package/dist/services/llm/custom.js.map +1 -0
- package/dist/services/llm/extract-json.d.ts +17 -0
- package/dist/services/llm/extract-json.d.ts.map +1 -0
- package/dist/services/llm/extract-json.js +60 -0
- package/dist/services/llm/extract-json.js.map +1 -0
- package/dist/services/llm/finish-reason.d.ts +21 -0
- package/dist/services/llm/finish-reason.d.ts.map +1 -0
- package/dist/services/llm/finish-reason.js +55 -0
- package/dist/services/llm/finish-reason.js.map +1 -0
- package/dist/services/llm/gemini.d.ts +26 -0
- package/dist/services/llm/gemini.d.ts.map +1 -0
- package/dist/services/llm/gemini.js +178 -0
- package/dist/services/llm/gemini.js.map +1 -0
- package/dist/services/llm/groq.d.ts +28 -0
- package/dist/services/llm/groq.d.ts.map +1 -0
- package/dist/services/llm/groq.js +237 -0
- package/dist/services/llm/groq.js.map +1 -0
- package/dist/services/llm/index.d.ts +19 -0
- package/dist/services/llm/index.d.ts.map +1 -0
- package/dist/services/llm/index.js +88 -0
- package/dist/services/llm/index.js.map +1 -0
- package/dist/services/llm/openai.d.ts +101 -0
- package/dist/services/llm/openai.d.ts.map +1 -0
- package/dist/services/llm/openai.js +561 -0
- package/dist/services/llm/openai.js.map +1 -0
- package/dist/services/llm/types.d.ts +110 -0
- package/dist/services/llm/types.d.ts.map +1 -0
- package/dist/services/llm/types.js +16 -0
- package/dist/services/llm/types.js.map +1 -0
- package/dist/types/index.d.ts +812 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +366 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull the JSON out of a model reply that is not obliged to return only JSON.
|
|
3
|
+
*
|
|
4
|
+
* Function calling guarantees a clean payload; prompt-instructed output does
|
|
5
|
+
* not. A model asked in words for JSON may wrap it in a markdown fence, preface
|
|
6
|
+
* it with a sentence, or — on a reasoning model — put a whole `<think>` block in
|
|
7
|
+
* front of it. This is the tolerant reader for that case.
|
|
8
|
+
*
|
|
9
|
+
* It lived as a private method on the `lm_studio` provider, where it was written
|
|
10
|
+
* for local servers that support neither `tools` nor `response_format`. It moved
|
|
11
|
+
* here when DeepSeek needed exactly the same treatment for the opposite reason:
|
|
12
|
+
* not a server too simple for function calling, but a model too *new* for it —
|
|
13
|
+
* DeepSeek V4 is a thinking model and its API rejects `tool_choice` outright.
|
|
14
|
+
* One reader, so a fix to either provider's parsing is a fix to both.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractJson(text: string): string;
|
|
17
|
+
//# sourceMappingURL=extract-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-json.d.ts","sourceRoot":"","sources":["../../../src/services/llm/extract-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6ChD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull the JSON out of a model reply that is not obliged to return only JSON.
|
|
3
|
+
*
|
|
4
|
+
* Function calling guarantees a clean payload; prompt-instructed output does
|
|
5
|
+
* not. A model asked in words for JSON may wrap it in a markdown fence, preface
|
|
6
|
+
* it with a sentence, or — on a reasoning model — put a whole `<think>` block in
|
|
7
|
+
* front of it. This is the tolerant reader for that case.
|
|
8
|
+
*
|
|
9
|
+
* It lived as a private method on the `lm_studio` provider, where it was written
|
|
10
|
+
* for local servers that support neither `tools` nor `response_format`. It moved
|
|
11
|
+
* here when DeepSeek needed exactly the same treatment for the opposite reason:
|
|
12
|
+
* not a server too simple for function calling, but a model too *new* for it —
|
|
13
|
+
* DeepSeek V4 is a thinking model and its API rejects `tool_choice` outright.
|
|
14
|
+
* One reader, so a fix to either provider's parsing is a fix to both.
|
|
15
|
+
*/
|
|
16
|
+
export function extractJson(text) {
|
|
17
|
+
// Thinking blocks first: a reasoning model emits them before the answer, and
|
|
18
|
+
// they routinely contain braces that would otherwise be mistaken for it.
|
|
19
|
+
let cleaned = text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
20
|
+
// A fenced block is the strongest signal, so try it before anything looser —
|
|
21
|
+
// but only accept it if it actually parses, since a model will occasionally
|
|
22
|
+
// fence its reasoning and leave the real answer outside.
|
|
23
|
+
const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
24
|
+
if (codeBlockMatch) {
|
|
25
|
+
try {
|
|
26
|
+
JSON.parse(codeBlockMatch[1].trim());
|
|
27
|
+
return codeBlockMatch[1].trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Not the payload. Fall through to the looser strategies.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
cleaned = cleaned
|
|
34
|
+
.replace(/```(?:json)?\s*/g, "")
|
|
35
|
+
.replace(/```/g, "")
|
|
36
|
+
.trim();
|
|
37
|
+
try {
|
|
38
|
+
JSON.parse(cleaned);
|
|
39
|
+
return cleaned;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Still wrapped in prose.
|
|
43
|
+
}
|
|
44
|
+
// The first balanced-looking object or array. Greedy on purpose: the payload
|
|
45
|
+
// is usually the largest structure present, and a lazy match would stop at
|
|
46
|
+
// the first nested closing brace.
|
|
47
|
+
const jsonMatch = cleaned.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
|
|
48
|
+
if (jsonMatch) {
|
|
49
|
+
try {
|
|
50
|
+
JSON.parse(jsonMatch[1]);
|
|
51
|
+
return jsonMatch[1];
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Nothing parseable. The caller reports the raw reply, which is more
|
|
55
|
+
// useful than a guess.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return cleaned;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=extract-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-json.js","sourceRoot":"","sources":["../../../src/services/llm/extract-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAEnE,6EAA6E;IAC7E,4EAA4E;IAC5E,yDAAyD;IACzD,MAAM,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;IACrE,IAAI,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YACtC,OAAO,cAAc,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;QAC5D,CAAC;IACH,CAAC;IAED,OAAO,GAAG,OAAO;SACd,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;SACnB,IAAI,EAAE,CAAC;IAEV,IAAI,CAAC;QACH,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACpB,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;IAC5B,CAAC;IAED,6EAA6E;IAC7E,2EAA2E;IAC3E,kCAAkC;IAClC,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC7D,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAE,CAAC,CAAC;YAC1B,OAAO,SAAS,CAAC,CAAC,CAAE,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;YACrE,uBAAuB;QACzB,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Finish-reason normalization across LLM providers
|
|
3
|
+
* @description Providers report why generation stopped under different names
|
|
4
|
+
* (`finish_reason`, `stop_reason`, `finishReason`) and with different
|
|
5
|
+
* vocabularies. This maps all of them onto the {@link FinishReason} union that
|
|
6
|
+
* ShapeShyft reports to callers.
|
|
7
|
+
*
|
|
8
|
+
* The distinction that matters is `length`: a truncated answer usually fails
|
|
9
|
+
* schema validation, and without this a caller diagnoses a truncation as a
|
|
10
|
+
* malformed model -- a different fault with a different correct fix.
|
|
11
|
+
*/
|
|
12
|
+
import type { FinishReason } from "../../types/index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Normalize a provider's stop reason.
|
|
15
|
+
*
|
|
16
|
+
* @param raw - The provider's raw value, in whatever casing it used
|
|
17
|
+
* @returns The normalized reason, `"other"` for an unrecognized string, or
|
|
18
|
+
* `undefined` when the provider reported nothing usable
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeFinishReason(raw: unknown): FinishReason | undefined;
|
|
21
|
+
//# sourceMappingURL=finish-reason.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finish-reason.d.ts","sourceRoot":"","sources":["../../../src/services/llm/finish-reason.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAqCzD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAK5E"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Finish-reason normalization across LLM providers
|
|
3
|
+
* @description Providers report why generation stopped under different names
|
|
4
|
+
* (`finish_reason`, `stop_reason`, `finishReason`) and with different
|
|
5
|
+
* vocabularies. This maps all of them onto the {@link FinishReason} union that
|
|
6
|
+
* ShapeShyft reports to callers.
|
|
7
|
+
*
|
|
8
|
+
* The distinction that matters is `length`: a truncated answer usually fails
|
|
9
|
+
* schema validation, and without this a caller diagnoses a truncation as a
|
|
10
|
+
* malformed model -- a different fault with a different correct fix.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Raw provider values, lowercased, mapped to normalized reasons.
|
|
14
|
+
*
|
|
15
|
+
* - OpenAI chat completions: `stop`, `length`, `tool_calls`, `content_filter`, `function_call`
|
|
16
|
+
* - OpenAI Responses API: `max_output_tokens`, `content_filter`
|
|
17
|
+
* - Anthropic: `end_turn`, `max_tokens`, `stop_sequence`, `tool_use`, `refusal`
|
|
18
|
+
* - Gemini: `STOP`, `MAX_TOKENS`, `SAFETY`, `RECITATION`, `OTHER`
|
|
19
|
+
*/
|
|
20
|
+
const FINISH_REASON_MAP = {
|
|
21
|
+
// Natural completion
|
|
22
|
+
stop: "stop",
|
|
23
|
+
end_turn: "stop",
|
|
24
|
+
stop_sequence: "stop",
|
|
25
|
+
complete: "stop",
|
|
26
|
+
// Hit the token ceiling -- the reason this normalization exists
|
|
27
|
+
length: "length",
|
|
28
|
+
max_tokens: "length",
|
|
29
|
+
max_output_tokens: "length",
|
|
30
|
+
// Stopped by a safety system
|
|
31
|
+
content_filter: "content_filter",
|
|
32
|
+
safety: "content_filter",
|
|
33
|
+
recitation: "content_filter",
|
|
34
|
+
refusal: "content_filter",
|
|
35
|
+
blocklist: "content_filter",
|
|
36
|
+
// Stopped to call a tool
|
|
37
|
+
tool_calls: "tool_calls",
|
|
38
|
+
tool_use: "tool_calls",
|
|
39
|
+
function_call: "tool_calls",
|
|
40
|
+
other: "other",
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Normalize a provider's stop reason.
|
|
44
|
+
*
|
|
45
|
+
* @param raw - The provider's raw value, in whatever casing it used
|
|
46
|
+
* @returns The normalized reason, `"other"` for an unrecognized string, or
|
|
47
|
+
* `undefined` when the provider reported nothing usable
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeFinishReason(raw) {
|
|
50
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return FINISH_REASON_MAP[raw.toLowerCase()] ?? "other";
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=finish-reason.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finish-reason.js","sourceRoot":"","sources":["../../../src/services/llm/finish-reason.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;GAOG;AACH,MAAM,iBAAiB,GAAiC;IACtD,qBAAqB;IACrB,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,MAAM;IAChB,aAAa,EAAE,MAAM;IACrB,QAAQ,EAAE,MAAM;IAEhB,gEAAgE;IAChE,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,QAAQ;IACpB,iBAAiB,EAAE,QAAQ;IAE3B,6BAA6B;IAC7B,cAAc,EAAE,gBAAgB;IAChC,MAAM,EAAE,gBAAgB;IACxB,UAAU,EAAE,gBAAgB;IAC5B,OAAO,EAAE,gBAAgB;IACzB,SAAS,EAAE,gBAAgB;IAE3B,yBAAyB;IACzB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,YAAY;IACtB,aAAa,EAAE,YAAY;IAE3B,KAAK,EAAE,OAAO;CACf,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,iBAAiB,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,OAAO,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Google Gemini LLM provider
|
|
3
|
+
* @description Implements the ILLMProvider interface for Gemini models.
|
|
4
|
+
* Uses native responseSchema for structured JSON output. Supports multimodal
|
|
5
|
+
* input (images, audio, video) via base64 and GCS URLs. Includes stubs
|
|
6
|
+
* for Imagen image generation and Veo video generation (requires Vertex AI SDK).
|
|
7
|
+
*/
|
|
8
|
+
import type { ILLMProvider, LLMRequest, LLMResponse, ProviderConfig } from "./types.js";
|
|
9
|
+
export declare class GeminiProvider implements ILLMProvider {
|
|
10
|
+
readonly providerName: "gemini";
|
|
11
|
+
private genAI;
|
|
12
|
+
private defaultModel;
|
|
13
|
+
constructor(config: ProviderConfig);
|
|
14
|
+
generate(request: LLMRequest): Promise<LLMResponse>;
|
|
15
|
+
/**
|
|
16
|
+
* Generate media (images or video) using Imagen or Veo models.
|
|
17
|
+
* Note: This is a simplified implementation - production would use Vertex AI SDK.
|
|
18
|
+
*/
|
|
19
|
+
private generateMedia;
|
|
20
|
+
/**
|
|
21
|
+
* Convert standard JSON Schema to Gemini's schema format
|
|
22
|
+
*/
|
|
23
|
+
private convertToGeminiSchema;
|
|
24
|
+
buildApiPayload(request: LLMRequest): Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=gemini.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../src/services/llm/gemini.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACX,cAAc,EACf,MAAM,YAAY,CAAC;AAMpB,qBAAa,cAAe,YAAW,YAAY;IACjD,QAAQ,CAAC,YAAY,EAAG,QAAQ,CAAU;IAC1C,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,EAAE,cAAc;IAQ5B,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC;IA2EzD;;;OAGG;YACW,aAAa;IAwD3B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAe7B,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAoC9D"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Google Gemini LLM provider
|
|
3
|
+
* @description Implements the ILLMProvider interface for Gemini models.
|
|
4
|
+
* Uses native responseSchema for structured JSON output. Supports multimodal
|
|
5
|
+
* input (images, audio, video) via base64 and GCS URLs. Includes stubs
|
|
6
|
+
* for Imagen image generation and Veo video generation (requires Vertex AI SDK).
|
|
7
|
+
*/
|
|
8
|
+
import { GoogleGenerativeAI, } from "@google/generative-ai";
|
|
9
|
+
import { isGenerativeModel } from "../../lib/capability-validator.js";
|
|
10
|
+
import { normalizeFinishReason } from "./finish-reason.js";
|
|
11
|
+
const DEFAULT_MODEL = "gemini-2.5-flash";
|
|
12
|
+
export class GeminiProvider {
|
|
13
|
+
providerName = "gemini";
|
|
14
|
+
genAI;
|
|
15
|
+
defaultModel;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
if (!config.apiKey) {
|
|
18
|
+
throw new Error("Gemini API key is required");
|
|
19
|
+
}
|
|
20
|
+
this.genAI = new GoogleGenerativeAI(config.apiKey);
|
|
21
|
+
this.defaultModel = config.model ?? DEFAULT_MODEL;
|
|
22
|
+
}
|
|
23
|
+
async generate(request) {
|
|
24
|
+
const modelName = request.model ?? this.defaultModel;
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
// Check if this is a generative model (Imagen, Veo)
|
|
27
|
+
if (isGenerativeModel(modelName)) {
|
|
28
|
+
return this.generateMedia(request, modelName, startTime);
|
|
29
|
+
}
|
|
30
|
+
// Build multimodal parts
|
|
31
|
+
const parts = [];
|
|
32
|
+
if (request.media?.length) {
|
|
33
|
+
for (const m of request.media) {
|
|
34
|
+
if (m.format === "base64") {
|
|
35
|
+
parts.push({
|
|
36
|
+
inlineData: { mimeType: m.mimeType, data: m.data },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (m.format === "url") {
|
|
40
|
+
// Only gs:// URLs are allowed (validated earlier)
|
|
41
|
+
parts.push({
|
|
42
|
+
fileData: { mimeType: m.mimeType, fileUri: m.data },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Add text prompt
|
|
48
|
+
parts.push({ text: request.prompt });
|
|
49
|
+
// Create model with system instruction
|
|
50
|
+
const model = this.genAI.getGenerativeModel({
|
|
51
|
+
model: modelName,
|
|
52
|
+
systemInstruction: request.systemPrompt,
|
|
53
|
+
});
|
|
54
|
+
// Configure for JSON output with schema
|
|
55
|
+
const generationConfig = {
|
|
56
|
+
responseMimeType: "application/json",
|
|
57
|
+
responseSchema: this.convertToGeminiSchema(request.outputSchema),
|
|
58
|
+
temperature: request.temperature ?? 0,
|
|
59
|
+
maxOutputTokens: request.maxTokens,
|
|
60
|
+
};
|
|
61
|
+
const result = await model.generateContent({
|
|
62
|
+
contents: [{ role: "user", parts }],
|
|
63
|
+
generationConfig,
|
|
64
|
+
});
|
|
65
|
+
const latencyMs = Date.now() - startTime;
|
|
66
|
+
const response = result.response;
|
|
67
|
+
const rawResponse = response.text();
|
|
68
|
+
const content = JSON.parse(rawResponse);
|
|
69
|
+
// Gemini usage metadata
|
|
70
|
+
const usageMetadata = response.usageMetadata;
|
|
71
|
+
return {
|
|
72
|
+
content,
|
|
73
|
+
rawResponse,
|
|
74
|
+
usage: {
|
|
75
|
+
promptTokens: usageMetadata?.promptTokenCount ?? 0,
|
|
76
|
+
completionTokens: usageMetadata?.candidatesTokenCount ?? 0,
|
|
77
|
+
totalTokens: usageMetadata?.totalTokenCount ?? 0,
|
|
78
|
+
},
|
|
79
|
+
model: modelName,
|
|
80
|
+
provider: this.providerName,
|
|
81
|
+
latencyMs,
|
|
82
|
+
finishReason: normalizeFinishReason(response.candidates?.[0]?.finishReason),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Generate media (images or video) using Imagen or Veo models.
|
|
87
|
+
* Note: This is a simplified implementation - production would use Vertex AI SDK.
|
|
88
|
+
*/
|
|
89
|
+
async generateMedia(_request, modelName, startTime) {
|
|
90
|
+
// Note: Imagen and Veo require Vertex AI SDK, not the basic genai SDK.
|
|
91
|
+
// This is a placeholder that shows the structure.
|
|
92
|
+
// In production, you would:
|
|
93
|
+
// 1. Use @google-cloud/vertexai package
|
|
94
|
+
// 2. Call imagen.generateImages() or veo.generateVideo()
|
|
95
|
+
// 3. Handle the response appropriately
|
|
96
|
+
if (modelName.includes("imagen")) {
|
|
97
|
+
// Placeholder for Imagen implementation
|
|
98
|
+
console.warn("Imagen generation requires Vertex AI SDK - not implemented in v1");
|
|
99
|
+
// For now, return a structured response indicating the limitation
|
|
100
|
+
return {
|
|
101
|
+
content: {
|
|
102
|
+
error: "Image generation requires Vertex AI SDK",
|
|
103
|
+
model: modelName,
|
|
104
|
+
},
|
|
105
|
+
rawResponse: JSON.stringify({ error: "Not implemented" }),
|
|
106
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
107
|
+
model: modelName,
|
|
108
|
+
provider: this.providerName,
|
|
109
|
+
latencyMs: Date.now() - startTime,
|
|
110
|
+
generatedMedia: undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (modelName.includes("veo")) {
|
|
114
|
+
// Placeholder for Veo implementation
|
|
115
|
+
console.warn("Veo generation requires Vertex AI SDK - not implemented in v1");
|
|
116
|
+
return {
|
|
117
|
+
content: {
|
|
118
|
+
error: "Video generation requires Vertex AI SDK",
|
|
119
|
+
model: modelName,
|
|
120
|
+
},
|
|
121
|
+
rawResponse: JSON.stringify({ error: "Not implemented" }),
|
|
122
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
123
|
+
model: modelName,
|
|
124
|
+
provider: this.providerName,
|
|
125
|
+
latencyMs: Date.now() - startTime,
|
|
126
|
+
generatedMedia: undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Unknown generative model: ${modelName}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Convert standard JSON Schema to Gemini's schema format
|
|
133
|
+
*/
|
|
134
|
+
convertToGeminiSchema(jsonSchema) {
|
|
135
|
+
// Gemini mostly accepts standard JSON Schema, but may need adjustments
|
|
136
|
+
const geminiSchema = { ...jsonSchema };
|
|
137
|
+
// Remove unsupported keywords
|
|
138
|
+
const unsupportedKeywords = ["$schema", "$id", "definitions", "$defs"];
|
|
139
|
+
for (const keyword of unsupportedKeywords) {
|
|
140
|
+
delete geminiSchema[keyword];
|
|
141
|
+
}
|
|
142
|
+
return geminiSchema;
|
|
143
|
+
}
|
|
144
|
+
buildApiPayload(request) {
|
|
145
|
+
const modelName = request.model ?? this.defaultModel;
|
|
146
|
+
// Build multimodal parts
|
|
147
|
+
const parts = [];
|
|
148
|
+
if (request.media?.length) {
|
|
149
|
+
for (const m of request.media) {
|
|
150
|
+
if (m.format === "base64") {
|
|
151
|
+
parts.push({
|
|
152
|
+
inlineData: { mimeType: m.mimeType, data: m.data },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (m.format === "url") {
|
|
156
|
+
parts.push({
|
|
157
|
+
fileData: { mimeType: m.mimeType, fileUri: m.data },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
parts.push({ text: request.prompt });
|
|
163
|
+
return {
|
|
164
|
+
model: modelName,
|
|
165
|
+
contents: [{ parts }],
|
|
166
|
+
systemInstruction: request.systemPrompt
|
|
167
|
+
? { parts: [{ text: request.systemPrompt }] }
|
|
168
|
+
: undefined,
|
|
169
|
+
generationConfig: {
|
|
170
|
+
responseMimeType: "application/json",
|
|
171
|
+
responseSchema: this.convertToGeminiSchema(request.outputSchema),
|
|
172
|
+
temperature: request.temperature ?? 0,
|
|
173
|
+
maxOutputTokens: request.maxTokens,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../src/services/llm/gemini.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groq LLM Provider
|
|
3
|
+
*
|
|
4
|
+
* Groq provides fast inference for various models including Whisper for transcription.
|
|
5
|
+
* For chat completions, Groq uses an OpenAI-compatible API format.
|
|
6
|
+
* For Whisper transcription, a dedicated implementation is needed.
|
|
7
|
+
*/
|
|
8
|
+
import type { ILLMProvider, LLMRequest, LLMResponse, ProviderConfig } from "./types.js";
|
|
9
|
+
export declare class GroqProvider implements ILLMProvider {
|
|
10
|
+
readonly providerName: "groq";
|
|
11
|
+
private client;
|
|
12
|
+
private defaultModel;
|
|
13
|
+
private apiKey;
|
|
14
|
+
constructor(config: ProviderConfig);
|
|
15
|
+
generate(request: LLMRequest): Promise<LLMResponse>;
|
|
16
|
+
/**
|
|
17
|
+
* Generate transcription using Whisper model.
|
|
18
|
+
* If an extraction model is configured, the transcription is fed through
|
|
19
|
+
* that model to produce structured output.
|
|
20
|
+
*/
|
|
21
|
+
private generateTranscription;
|
|
22
|
+
/**
|
|
23
|
+
* Generate chat completion using Groq's OpenAI-compatible API.
|
|
24
|
+
*/
|
|
25
|
+
private generateChatCompletion;
|
|
26
|
+
buildApiPayload(request: LLMRequest): Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=groq.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"groq.d.ts","sourceRoot":"","sources":["../../../src/services/llm/groq.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,WAAW,EACX,cAAc,EACf,MAAM,YAAY,CAAC;AA4CpB,qBAAa,YAAa,YAAW,YAAY;IAC/C,QAAQ,CAAC,YAAY,EAAG,MAAM,CAAU;IACxC,OAAO,CAAC,MAAM,CAAO;IACrB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,cAAc;IAS5B,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC;IAYzD;;;;OAIG;YACW,qBAAqB;IAmGnC;;OAEG;YACW,sBAAsB;IAmEpC,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAyC9D"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groq LLM Provider
|
|
3
|
+
*
|
|
4
|
+
* Groq provides fast inference for various models including Whisper for transcription.
|
|
5
|
+
* For chat completions, Groq uses an OpenAI-compatible API format.
|
|
6
|
+
* For Whisper transcription, a dedicated implementation is needed.
|
|
7
|
+
*/
|
|
8
|
+
import Groq from "groq-sdk";
|
|
9
|
+
import { toFile } from "groq-sdk/uploads";
|
|
10
|
+
import { createLLMProvider } from "./index.js";
|
|
11
|
+
import { getProviderForModel } from "../../config/providers.js";
|
|
12
|
+
import { isTranscriptionModel } from "../../lib/capability-validator.js";
|
|
13
|
+
import { normalizeFinishReason } from "./finish-reason.js";
|
|
14
|
+
const DEFAULT_MODEL = "llama-3.3-70b-versatile";
|
|
15
|
+
const DEFAULT_WHISPER_MODEL = "whisper-large-v3";
|
|
16
|
+
/**
|
|
17
|
+
* Convert base64 audio data to an Uploadable for the Groq SDK.
|
|
18
|
+
*/
|
|
19
|
+
async function base64ToUploadable(base64Data, mimeType, fieldName) {
|
|
20
|
+
// Validate base64 before decoding
|
|
21
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(base64Data)) {
|
|
22
|
+
throw new Error("Invalid base64 encoding in audio data");
|
|
23
|
+
}
|
|
24
|
+
// Decode base64 to binary
|
|
25
|
+
const binaryString = atob(base64Data);
|
|
26
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
27
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
28
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
29
|
+
}
|
|
30
|
+
// Validate buffer is not empty
|
|
31
|
+
if (bytes.length === 0) {
|
|
32
|
+
throw new Error("Audio data decoded to empty buffer");
|
|
33
|
+
}
|
|
34
|
+
// Determine file extension from MIME type
|
|
35
|
+
const extension = mimeType.split("/")[1] ?? "mp3";
|
|
36
|
+
const filename = fieldName
|
|
37
|
+
? `${fieldName}.${extension}`
|
|
38
|
+
: `audio.${extension}`;
|
|
39
|
+
// Use Groq SDK's toFile utility to create a proper Uploadable
|
|
40
|
+
return toFile(bytes, filename, { type: mimeType });
|
|
41
|
+
}
|
|
42
|
+
export class GroqProvider {
|
|
43
|
+
providerName = "groq";
|
|
44
|
+
client;
|
|
45
|
+
defaultModel;
|
|
46
|
+
apiKey;
|
|
47
|
+
constructor(config) {
|
|
48
|
+
if (!config.apiKey) {
|
|
49
|
+
throw new Error("Groq API key is required");
|
|
50
|
+
}
|
|
51
|
+
this.apiKey = config.apiKey;
|
|
52
|
+
this.client = new Groq({ apiKey: config.apiKey });
|
|
53
|
+
this.defaultModel = config.model ?? DEFAULT_MODEL;
|
|
54
|
+
}
|
|
55
|
+
async generate(request) {
|
|
56
|
+
const model = request.model ?? this.defaultModel;
|
|
57
|
+
// Check if this is a transcription model (Whisper)
|
|
58
|
+
if (isTranscriptionModel(model)) {
|
|
59
|
+
return this.generateTranscription(request, model);
|
|
60
|
+
}
|
|
61
|
+
// For non-Whisper models, use chat completions (OpenAI-compatible)
|
|
62
|
+
return this.generateChatCompletion(request, model);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate transcription using Whisper model.
|
|
66
|
+
* If an extraction model is configured, the transcription is fed through
|
|
67
|
+
* that model to produce structured output.
|
|
68
|
+
*/
|
|
69
|
+
async generateTranscription(request, model) {
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
// Validate: exactly one audio input
|
|
72
|
+
const audioMedia = request.media?.filter(m => m.type === "audio");
|
|
73
|
+
if (!audioMedia || audioMedia.length === 0) {
|
|
74
|
+
throw new Error("Whisper requires exactly one audio input");
|
|
75
|
+
}
|
|
76
|
+
if (audioMedia.length > 1) {
|
|
77
|
+
throw new Error(`Whisper accepts only one audio input, got ${audioMedia.length}`);
|
|
78
|
+
}
|
|
79
|
+
const audio = audioMedia[0];
|
|
80
|
+
// Convert base64 to Uploadable for SDK
|
|
81
|
+
let audioFile;
|
|
82
|
+
try {
|
|
83
|
+
audioFile = await base64ToUploadable(audio.data, audio.mimeType, audio.fieldName);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`Invalid audio data: ${error instanceof Error ? error.message : error}`);
|
|
87
|
+
}
|
|
88
|
+
// Transcribe using Groq Whisper
|
|
89
|
+
const transcription = await this.client.audio.transcriptions.create({
|
|
90
|
+
file: audioFile,
|
|
91
|
+
model: model || DEFAULT_WHISPER_MODEL,
|
|
92
|
+
});
|
|
93
|
+
const transcriptionText = transcription.text;
|
|
94
|
+
const transcriptionLatency = Date.now() - startTime;
|
|
95
|
+
// If no extraction model configured, return raw transcription
|
|
96
|
+
if (!request.extractionModel) {
|
|
97
|
+
return {
|
|
98
|
+
content: { transcription: transcriptionText },
|
|
99
|
+
rawResponse: transcriptionText,
|
|
100
|
+
usage: {
|
|
101
|
+
promptTokens: 0,
|
|
102
|
+
completionTokens: 0,
|
|
103
|
+
totalTokens: 0,
|
|
104
|
+
},
|
|
105
|
+
model: model || DEFAULT_WHISPER_MODEL,
|
|
106
|
+
provider: this.providerName,
|
|
107
|
+
latencyMs: transcriptionLatency,
|
|
108
|
+
// Whisper transcribes to completion; there is no token ceiling to hit.
|
|
109
|
+
finishReason: "stop",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Feed transcription through extraction model for structured output
|
|
113
|
+
const extractionProvider = createLLMProvider(getProviderForModel(request.extractionModel), {
|
|
114
|
+
apiKey: request.extractionApiKey,
|
|
115
|
+
model: request.extractionModel,
|
|
116
|
+
});
|
|
117
|
+
// Build extraction prompt that includes the transcription
|
|
118
|
+
const extractionPrompt = request.prompt
|
|
119
|
+
? `${request.prompt}\n\nTranscription:\n${transcriptionText}`
|
|
120
|
+
: `Extract structured data from this transcription:\n\n${transcriptionText}`;
|
|
121
|
+
const extractionRequest = {
|
|
122
|
+
prompt: extractionPrompt,
|
|
123
|
+
systemPrompt: request.systemPrompt,
|
|
124
|
+
outputSchema: request.outputSchema,
|
|
125
|
+
model: request.extractionModel,
|
|
126
|
+
temperature: request.temperature,
|
|
127
|
+
maxTokens: request.maxTokens,
|
|
128
|
+
};
|
|
129
|
+
const extractionResponse = await extractionProvider.generate(extractionRequest);
|
|
130
|
+
// Combine latency from both steps
|
|
131
|
+
return {
|
|
132
|
+
...extractionResponse,
|
|
133
|
+
// Override latency to include both transcription and extraction
|
|
134
|
+
latencyMs: Date.now() - startTime,
|
|
135
|
+
// Include raw transcription in response for debugging
|
|
136
|
+
rawResponse: JSON.stringify({
|
|
137
|
+
transcription: transcriptionText,
|
|
138
|
+
extraction: extractionResponse.rawResponse,
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Generate chat completion using Groq's OpenAI-compatible API.
|
|
144
|
+
*/
|
|
145
|
+
async generateChatCompletion(request, model) {
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
// Build messages
|
|
148
|
+
const messages = [];
|
|
149
|
+
if (request.systemPrompt) {
|
|
150
|
+
messages.push({ role: "system", content: request.systemPrompt });
|
|
151
|
+
}
|
|
152
|
+
// For Groq chat models, we don't have native multimodal support
|
|
153
|
+
// Media should have been extracted and replaced with placeholders
|
|
154
|
+
messages.push({ role: "user", content: request.prompt });
|
|
155
|
+
// Use function calling for structured output
|
|
156
|
+
const tools = [
|
|
157
|
+
{
|
|
158
|
+
type: "function",
|
|
159
|
+
function: {
|
|
160
|
+
name: "structured_response",
|
|
161
|
+
description: "Generate structured response matching the schema",
|
|
162
|
+
parameters: request.outputSchema,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
const response = await this.client.chat.completions.create({
|
|
167
|
+
model,
|
|
168
|
+
messages,
|
|
169
|
+
tools,
|
|
170
|
+
tool_choice: {
|
|
171
|
+
type: "function",
|
|
172
|
+
function: { name: "structured_response" },
|
|
173
|
+
},
|
|
174
|
+
temperature: request.temperature ?? 0,
|
|
175
|
+
max_tokens: request.maxTokens,
|
|
176
|
+
});
|
|
177
|
+
const latencyMs = Date.now() - startTime;
|
|
178
|
+
// Extract structured response from function call
|
|
179
|
+
const toolCall = response.choices[0]?.message.tool_calls?.[0];
|
|
180
|
+
if (!toolCall || toolCall.function.name !== "structured_response") {
|
|
181
|
+
throw new Error("Expected function call response from Groq");
|
|
182
|
+
}
|
|
183
|
+
const rawResponse = toolCall.function.arguments;
|
|
184
|
+
const content = JSON.parse(rawResponse);
|
|
185
|
+
return {
|
|
186
|
+
content,
|
|
187
|
+
rawResponse,
|
|
188
|
+
usage: {
|
|
189
|
+
promptTokens: response.usage?.prompt_tokens ?? 0,
|
|
190
|
+
completionTokens: response.usage?.completion_tokens ?? 0,
|
|
191
|
+
totalTokens: response.usage?.total_tokens ?? 0,
|
|
192
|
+
},
|
|
193
|
+
model: response.model,
|
|
194
|
+
provider: this.providerName,
|
|
195
|
+
latencyMs,
|
|
196
|
+
finishReason: normalizeFinishReason(response.choices[0]?.finish_reason),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
buildApiPayload(request) {
|
|
200
|
+
const model = request.model ?? this.defaultModel;
|
|
201
|
+
// For Whisper, return transcription request format
|
|
202
|
+
if (isTranscriptionModel(model)) {
|
|
203
|
+
return {
|
|
204
|
+
model,
|
|
205
|
+
// Note: file would be added separately as multipart form data
|
|
206
|
+
response_format: "json",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// For chat models, return chat completion format
|
|
210
|
+
const messages = [];
|
|
211
|
+
if (request.systemPrompt) {
|
|
212
|
+
messages.push({ role: "system", content: request.systemPrompt });
|
|
213
|
+
}
|
|
214
|
+
messages.push({ role: "user", content: request.prompt });
|
|
215
|
+
return {
|
|
216
|
+
model,
|
|
217
|
+
messages,
|
|
218
|
+
tools: [
|
|
219
|
+
{
|
|
220
|
+
type: "function",
|
|
221
|
+
function: {
|
|
222
|
+
name: "structured_response",
|
|
223
|
+
description: "Generate structured response matching the schema",
|
|
224
|
+
parameters: request.outputSchema,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
tool_choice: {
|
|
229
|
+
type: "function",
|
|
230
|
+
function: { name: "structured_response" },
|
|
231
|
+
},
|
|
232
|
+
temperature: request.temperature ?? 0,
|
|
233
|
+
max_tokens: request.maxTokens,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=groq.js.map
|