mohdel 0.90.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/LICENSE +21 -0
- package/README.md +377 -0
- package/config/benchmarks.json +39 -0
- package/js/client/call.js +75 -0
- package/js/client/call_image.js +82 -0
- package/js/client/gate-binary.js +72 -0
- package/js/client/index.js +16 -0
- package/js/client/ndjson.js +29 -0
- package/js/client/transport.js +48 -0
- package/js/core/envelope.js +141 -0
- package/js/core/errors.js +75 -0
- package/js/core/events.js +96 -0
- package/js/core/image.js +58 -0
- package/js/core/index.js +10 -0
- package/js/core/status.js +48 -0
- package/js/factory/bridge.js +372 -0
- package/js/session/_cooldown.js +114 -0
- package/js/session/_logger.js +138 -0
- package/js/session/_rate_limiter.js +77 -0
- package/js/session/_tracing.js +58 -0
- package/js/session/adapters/_cancelled.js +44 -0
- package/js/session/adapters/_catalog.js +58 -0
- package/js/session/adapters/_chat_completions.js +439 -0
- package/js/session/adapters/_errors.js +85 -0
- package/js/session/adapters/_images.js +60 -0
- package/js/session/adapters/_lazy_json_cache.js +76 -0
- package/js/session/adapters/_pricing.js +67 -0
- package/js/session/adapters/_providers.js +60 -0
- package/js/session/adapters/_tools.js +185 -0
- package/js/session/adapters/_videos.js +283 -0
- package/js/session/adapters/anthropic.js +397 -0
- package/js/session/adapters/cerebras.js +28 -0
- package/js/session/adapters/deepseek.js +32 -0
- package/js/session/adapters/echo.js +51 -0
- package/js/session/adapters/fake.js +262 -0
- package/js/session/adapters/fireworks.js +46 -0
- package/js/session/adapters/gemini.js +381 -0
- package/js/session/adapters/groq.js +23 -0
- package/js/session/adapters/image/fake.js +55 -0
- package/js/session/adapters/image/index.js +40 -0
- package/js/session/adapters/image/novita.js +135 -0
- package/js/session/adapters/image/openai.js +50 -0
- package/js/session/adapters/index.js +53 -0
- package/js/session/adapters/mistral.js +31 -0
- package/js/session/adapters/novita.js +29 -0
- package/js/session/adapters/openai.js +381 -0
- package/js/session/adapters/openrouter.js +66 -0
- package/js/session/adapters/xai.js +27 -0
- package/js/session/bin.js +54 -0
- package/js/session/driver.js +160 -0
- package/js/session/index.js +18 -0
- package/js/session/run.js +393 -0
- package/js/session/run_image.js +61 -0
- package/package.json +107 -0
- package/src/cli/ask.js +160 -0
- package/src/cli/backup.js +107 -0
- package/src/cli/bench.js +262 -0
- package/src/cli/check.js +123 -0
- package/src/cli/colored-logger.js +67 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/default.js +39 -0
- package/src/cli/index.js +150 -0
- package/src/cli/json-output.js +60 -0
- package/src/cli/model.js +571 -0
- package/src/cli/onboard.js +232 -0
- package/src/cli/rank.js +176 -0
- package/src/cli/ratelimit.js +160 -0
- package/src/cli/tag.js +105 -0
- package/src/lib/assets/alibaba.svg +1 -0
- package/src/lib/assets/anthropic.svg +5 -0
- package/src/lib/assets/deepseek.svg +1 -0
- package/src/lib/assets/gemini.svg +1 -0
- package/src/lib/assets/google.svg +2 -0
- package/src/lib/assets/kwaipilot.svg +1 -0
- package/src/lib/assets/meta.svg +1 -0
- package/src/lib/assets/minimax.svg +9 -0
- package/src/lib/assets/moonshotai.svg +4 -0
- package/src/lib/assets/openai.svg +5 -0
- package/src/lib/assets/xai.svg +1 -0
- package/src/lib/assets/xiaomi.svg +2 -0
- package/src/lib/assets/zai.svg +219 -0
- package/src/lib/benchmark-score.js +215 -0
- package/src/lib/benchmark-truth.js +68 -0
- package/src/lib/cache.js +76 -0
- package/src/lib/common.js +208 -0
- package/src/lib/cooldown.js +63 -0
- package/src/lib/creators.js +71 -0
- package/src/lib/curated-cache.js +146 -0
- package/src/lib/errors.js +126 -0
- package/src/lib/index.js +726 -0
- package/src/lib/logger.js +29 -0
- package/src/lib/providers.js +87 -0
- package/src/lib/rank.js +390 -0
- package/src/lib/rate-limiter.js +50 -0
- package/src/lib/schema.js +150 -0
- package/src/lib/select.js +474 -0
- package/src/lib/tracing.js +62 -0
- package/src/lib/utils.js +85 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image URI loader.
|
|
3
|
+
*
|
|
4
|
+
* Three URI schemes are supported (see INTEGRATION.md §Vision):
|
|
5
|
+
* - `file://` → reads from local filesystem, base64-encodes
|
|
6
|
+
* - `https://` → passed as URL reference (where the provider accepts it)
|
|
7
|
+
* - `data:` → base64 data URI parsed inline
|
|
8
|
+
*
|
|
9
|
+
* Each adapter calls `loadImages(images)` to get a normalized
|
|
10
|
+
* intermediate shape `{mimeType, base64?, url?}`, then formats per
|
|
11
|
+
* provider. Errors are surfaced — file IO failures bubble up so the
|
|
12
|
+
* adapter can emit a typed error rather than silently skipping.
|
|
13
|
+
*
|
|
14
|
+
* @module session/adapters/_images
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFile } from 'node:fs/promises'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} LoadedImage
|
|
21
|
+
* @property {string} mimeType
|
|
22
|
+
* @property {string} [base64] Raw base64 (no `data:` prefix)
|
|
23
|
+
* @property {string} [url] Remote URL (only when source was https://)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {Array<{fileUri: string, mimeType: string}>} images
|
|
28
|
+
* @returns {Promise<LoadedImage[]>}
|
|
29
|
+
*/
|
|
30
|
+
export async function loadImages (images) {
|
|
31
|
+
if (!images || !Array.isArray(images)) return []
|
|
32
|
+
const out = []
|
|
33
|
+
for (const img of images) {
|
|
34
|
+
if (!img?.fileUri || !img?.mimeType) continue
|
|
35
|
+
out.push(await loadImage(img))
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{fileUri: string, mimeType: string}} image
|
|
42
|
+
* @returns {Promise<LoadedImage>}
|
|
43
|
+
*/
|
|
44
|
+
export async function loadImage (image) {
|
|
45
|
+
const { fileUri, mimeType } = image
|
|
46
|
+
if (fileUri.startsWith('file://')) {
|
|
47
|
+
const path = fileUri.replace(/^file:\/\//, '')
|
|
48
|
+
const buf = await readFile(path)
|
|
49
|
+
return { mimeType, base64: buf.toString('base64') }
|
|
50
|
+
}
|
|
51
|
+
if (fileUri.startsWith('data:')) {
|
|
52
|
+
const parts = fileUri.split(',')
|
|
53
|
+
if (parts.length > 1) return { mimeType, base64: parts[1] }
|
|
54
|
+
throw new Error(`malformed data URI: ${fileUri.slice(0, 32)}…`)
|
|
55
|
+
}
|
|
56
|
+
if (fileUri.startsWith('https://') || fileUri.startsWith('http://')) {
|
|
57
|
+
return { mimeType, url: fileUri }
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`unsupported image URI scheme: ${fileUri.slice(0, 32)}…`)
|
|
60
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared lazy-load-once JSON cache for config files under
|
|
3
|
+
* `~/.config/mohdel/`. `_catalog.js` and `_providers.js` both had
|
|
4
|
+
* byte-similar implementations before F62; this helper owns the
|
|
5
|
+
* pattern.
|
|
6
|
+
*
|
|
7
|
+
* Contract:
|
|
8
|
+
* - `loadSync(path?)` — synchronous read; used as the lazy
|
|
9
|
+
* fallback inside `get()` and by tests that want to parse an
|
|
10
|
+
* arbitrary file without touching the shared cache.
|
|
11
|
+
* - `initAsync()` — idempotent eager init from the default path.
|
|
12
|
+
* Called from `bin.js::main` before `drive()` so the first
|
|
13
|
+
* `get()` doesn't stall the event loop on a sync read.
|
|
14
|
+
* - `set(table)` — replace the in-memory table (tests + extension
|
|
15
|
+
* hook for deployments that source config from elsewhere).
|
|
16
|
+
* - `get(key)` — read-through; loads synchronously on first miss.
|
|
17
|
+
*
|
|
18
|
+
* A malformed / missing / non-object file resolves to the supplied
|
|
19
|
+
* `defaultValue` (default `{}`) so callers never have to handle
|
|
20
|
+
* file-absence explicitly.
|
|
21
|
+
*
|
|
22
|
+
* @module session/adapters/_lazy_json_cache
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @template V
|
|
29
|
+
* @param {() => string} pathFn Resolves the default file path.
|
|
30
|
+
* @param {{defaultValue?: V}} [opts]
|
|
31
|
+
*/
|
|
32
|
+
export function createLazyJsonFileCache (pathFn, { defaultValue = /** @type {any} */({}) } = {}) {
|
|
33
|
+
/** @type {V | null} */
|
|
34
|
+
let active = null
|
|
35
|
+
|
|
36
|
+
/** @param {unknown} parsed */
|
|
37
|
+
function normalize (parsed) {
|
|
38
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
39
|
+
return defaultValue
|
|
40
|
+
}
|
|
41
|
+
return /** @type {V} */(parsed)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {string} [p] */
|
|
45
|
+
function loadSync (p) {
|
|
46
|
+
const file = p ?? pathFn()
|
|
47
|
+
try {
|
|
48
|
+
return normalize(JSON.parse(fs.readFileSync(file, 'utf8')))
|
|
49
|
+
} catch {
|
|
50
|
+
return defaultValue
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function initAsync () {
|
|
55
|
+
if (active !== null) return
|
|
56
|
+
try {
|
|
57
|
+
const text = await fs.promises.readFile(pathFn(), 'utf8')
|
|
58
|
+
active = normalize(JSON.parse(text))
|
|
59
|
+
} catch {
|
|
60
|
+
active = defaultValue
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {V} table */
|
|
65
|
+
function set (table) {
|
|
66
|
+
active = /** @type {V} */({ ...table })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @param {string} key */
|
|
70
|
+
function get (key) {
|
|
71
|
+
if (active === null) active = loadSync()
|
|
72
|
+
return active[key]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { loadSync, initAsync, set, get }
|
|
76
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost computation from curated catalog spec.
|
|
3
|
+
*
|
|
4
|
+
* Reads `inputPrice` / `outputPrice` / `thinkingPrice` from the
|
|
5
|
+
* catalog spec (loaded via `_catalog.js`). Returns a single number
|
|
6
|
+
* (USD) written to `AnswerResult.cost`. Unknown models or specs
|
|
7
|
+
* without prices return `0` — graceful degradation.
|
|
8
|
+
*
|
|
9
|
+
* @module session/adapters/_pricing
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getSpec, setCatalog } from './_catalog.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pure cost computation from spec + usage.
|
|
16
|
+
*
|
|
17
|
+
* @param {any} spec Catalog entry (with `inputPrice`/`outputPrice`/`thinkingPrice`),
|
|
18
|
+
* or `undefined`.
|
|
19
|
+
* @param {{inputTokens?: number, outputTokens?: number, thinkingTokens?: number}} usage
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
export function computeCost (spec, usage) {
|
|
23
|
+
if (!spec) return 0
|
|
24
|
+
const ip = spec.inputPrice
|
|
25
|
+
const op = spec.outputPrice
|
|
26
|
+
if (typeof ip !== 'number' || typeof op !== 'number') return 0
|
|
27
|
+
const i = usage.inputTokens ?? 0
|
|
28
|
+
const o = usage.outputTokens ?? 0
|
|
29
|
+
const t = usage.thinkingTokens ?? 0
|
|
30
|
+
const tp = typeof spec.thinkingPrice === 'number' ? spec.thinkingPrice : op
|
|
31
|
+
const total = (i * ip + o * op + t * tp) / 1_000_000
|
|
32
|
+
return round(total)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} model Fully-qualified `<provider>/<model>`.
|
|
37
|
+
* @param {{inputTokens?: number, outputTokens?: number, thinkingTokens?: number}} usage
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
export function costFor (model, usage) {
|
|
41
|
+
return computeCost(getSpec(model), usage)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Test convenience: inject pricing-only specs by model id. Wraps
|
|
46
|
+
* `setCatalog` with the `{input, output, thinking?}` shape used in
|
|
47
|
+
* existing tests, translating to spec fields.
|
|
48
|
+
*
|
|
49
|
+
* @param {Record<string, {input: number, output: number, thinking?: number}>} table
|
|
50
|
+
*/
|
|
51
|
+
export function setPricing (table) {
|
|
52
|
+
/** @type {Record<string, any>} */
|
|
53
|
+
const wrapped = {}
|
|
54
|
+
for (const [k, v] of Object.entries(table)) {
|
|
55
|
+
wrapped[k] = {
|
|
56
|
+
inputPrice: v.input,
|
|
57
|
+
outputPrice: v.output,
|
|
58
|
+
...(v.thinking != null && { thinkingPrice: v.thinking })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
setCatalog(wrapped)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {number} n */
|
|
65
|
+
function round (n) {
|
|
66
|
+
return Math.round(n * 1e6) / 1e6
|
|
67
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-level configuration reader.
|
|
3
|
+
*
|
|
4
|
+
* Per-provider rate limits live in `~/.config/mohdel/providers.json`
|
|
5
|
+
* as `{ <provider>: { rpmLimit, tpmLimit } }`. These are
|
|
6
|
+
* per-account values (different plans get different limits), so
|
|
7
|
+
* they live in user config — not source code.
|
|
8
|
+
*
|
|
9
|
+
* Sessions load once and cache in-process.
|
|
10
|
+
*
|
|
11
|
+
* @module session/adapters/_providers
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import envPaths from 'env-paths'
|
|
15
|
+
|
|
16
|
+
import { createLazyJsonFileCache } from './_lazy_json_cache.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} ProviderLimits
|
|
20
|
+
* @property {number} [rpmLimit]
|
|
21
|
+
* @property {number} [tpmLimit]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const cache = createLazyJsonFileCache(
|
|
25
|
+
// `{ suffix: null }` — see `_catalog.js` for the rationale (stay
|
|
26
|
+
// in lockstep with the CLI's `CONFIG_DIR`, avoid the `-nodejs`
|
|
27
|
+
// suffix env-paths appends by default).
|
|
28
|
+
() => `${envPaths('mohdel', { suffix: null }).config}/providers.json`
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} path
|
|
33
|
+
* @returns {Record<string, ProviderLimits>}
|
|
34
|
+
*/
|
|
35
|
+
export function loadProviders (path) {
|
|
36
|
+
return cache.loadSync(path)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Eager async initialization from the default providers path. Called
|
|
41
|
+
* from `bin.js::main` before `drive()` so `getProviderLimits` doesn't
|
|
42
|
+
* stall the event loop on a sync read mid-call. Idempotent; respects
|
|
43
|
+
* a prior `setProviders` (tests).
|
|
44
|
+
*/
|
|
45
|
+
export async function initProvidersFromDefault () {
|
|
46
|
+
await cache.initAsync()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {Record<string, ProviderLimits>} table */
|
|
50
|
+
export function setProviders (table) {
|
|
51
|
+
cache.set(table)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} provider
|
|
56
|
+
* @returns {ProviderLimits | undefined}
|
|
57
|
+
*/
|
|
58
|
+
export function getProviderLimits (provider) {
|
|
59
|
+
return cache.get(provider)
|
|
60
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool format conversion — provider-agnostic JSON-shape converters
|
|
3
|
+
* used by adapters to translate between the unified `ToolSpec` /
|
|
4
|
+
* `ToolCall` envelope shapes and each provider's native wire format.
|
|
5
|
+
*
|
|
6
|
+
* @module session/adapters/_tools
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const argsObj = (args) => args || {}
|
|
10
|
+
|
|
11
|
+
// Tool argument parse failures are expected (models routinely send
|
|
12
|
+
// malformed JSON before retrying with corrections). Fall back to
|
|
13
|
+
// returning the raw string — downstream adapter code handles the
|
|
14
|
+
// type mismatch. Logging here would create warn-level noise.
|
|
15
|
+
const parseArgs = (_name, args) => {
|
|
16
|
+
if (typeof args !== 'string') return argsObj(args)
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(args)
|
|
19
|
+
} catch {
|
|
20
|
+
return args
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert unified tool format to Anthropic's native format.
|
|
26
|
+
* @param {Array} tools Array of unified tool definitions.
|
|
27
|
+
* @returns {Array} Anthropic-formatted tools.
|
|
28
|
+
*/
|
|
29
|
+
export const toAnthropicTools = (tools) => tools.map(t => ({
|
|
30
|
+
name: t.name,
|
|
31
|
+
description: t.description,
|
|
32
|
+
input_schema: t.parameters
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert unified tool format to OpenAI's native format.
|
|
37
|
+
* @param {Array} tools Array of unified tool definitions.
|
|
38
|
+
* @returns {Array} OpenAI-formatted tools.
|
|
39
|
+
*/
|
|
40
|
+
export const toOpenAITools = (tools) => tools.map(t => ({
|
|
41
|
+
type: 'function',
|
|
42
|
+
name: t.name,
|
|
43
|
+
description: t.description,
|
|
44
|
+
parameters: t.parameters
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert unified tool format to Cerebras's native format (classic
|
|
49
|
+
* OpenAI chat completions).
|
|
50
|
+
* @param {Array} tools Array of unified tool definitions.
|
|
51
|
+
* @returns {Array} Cerebras-formatted tools.
|
|
52
|
+
*/
|
|
53
|
+
export const toCerebrasTools = (tools) => tools.map(t => ({
|
|
54
|
+
type: 'function',
|
|
55
|
+
function: {
|
|
56
|
+
name: t.name,
|
|
57
|
+
description: t.description,
|
|
58
|
+
parameters: t.parameters
|
|
59
|
+
}
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert unified tool format to Gemini's native format (wrapped in
|
|
64
|
+
* `functionDeclarations`).
|
|
65
|
+
* @param {Array} tools Array of unified tool definitions.
|
|
66
|
+
* @returns {Array} Gemini-formatted tools.
|
|
67
|
+
*/
|
|
68
|
+
export const toGeminiTools = (tools) => [{
|
|
69
|
+
functionDeclarations: tools.map(t => ({
|
|
70
|
+
name: t.name,
|
|
71
|
+
description: t.description,
|
|
72
|
+
parameters: t.parameters
|
|
73
|
+
}))
|
|
74
|
+
}]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalize Anthropic tool_use blocks to unified format.
|
|
78
|
+
* @param {Array} blocks Array of `tool_use` content blocks.
|
|
79
|
+
* @returns {Array} Unified toolCalls format.
|
|
80
|
+
*/
|
|
81
|
+
export const fromAnthropicToolCalls = (blocks) => blocks.map(block => ({
|
|
82
|
+
id: block.id,
|
|
83
|
+
name: block.name,
|
|
84
|
+
arguments: block.input
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalize OpenAI function calls to unified format.
|
|
89
|
+
* @param {Array} calls Array of OpenAI function call outputs.
|
|
90
|
+
* @returns {Array} Unified toolCalls format.
|
|
91
|
+
*/
|
|
92
|
+
export const fromOpenAIToolCalls = (calls) => calls.map(call => ({
|
|
93
|
+
id: call.call_id || call.id,
|
|
94
|
+
name: call.name,
|
|
95
|
+
arguments: parseArgs(call.name, call.arguments)
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Normalize Cerebras tool calls to unified format. Cerebras uses
|
|
100
|
+
* the classic OpenAI chat-completions shape `{id, function: {name,
|
|
101
|
+
* arguments}}`.
|
|
102
|
+
* @param {Array} calls Array of Cerebras tool_calls.
|
|
103
|
+
* @returns {Array} Unified toolCalls format.
|
|
104
|
+
*/
|
|
105
|
+
export const fromCerebrasToolCalls = (calls) => calls.map(call => ({
|
|
106
|
+
id: call.id,
|
|
107
|
+
name: call.function.name,
|
|
108
|
+
arguments: parseArgs(call.function.name, call.function.arguments)
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Normalize Gemini function calls to unified format. Gemini doesn't
|
|
113
|
+
* provide IDs, so we generate them.
|
|
114
|
+
* @param {Array} calls Array of Gemini `functionCall` parts.
|
|
115
|
+
* @returns {Array} Unified toolCalls format.
|
|
116
|
+
*/
|
|
117
|
+
export const fromGeminiToolCalls = (calls) => calls.map((call, index) => {
|
|
118
|
+
const tc = {
|
|
119
|
+
id: `gemini_call_${Date.now()}_${index}_${Math.random().toString(36).slice(2, 6)}`,
|
|
120
|
+
name: call.name,
|
|
121
|
+
arguments: call.args || {}
|
|
122
|
+
}
|
|
123
|
+
if (call.thoughtSignature) tc.thoughtSignature = call.thoughtSignature
|
|
124
|
+
return tc
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert tool choice to provider-specific format.
|
|
129
|
+
* @param {string} provider Provider name (`'anthropic'`, `'openai'`,
|
|
130
|
+
* `'cerebras'`, `'mistral'`, `'gemini'`).
|
|
131
|
+
* @param {string|object} choice Tool choice (`'auto'`, `'required'`,
|
|
132
|
+
* `'none'`, or a specific tool name).
|
|
133
|
+
* @returns {any} Provider-formatted tool choice.
|
|
134
|
+
*/
|
|
135
|
+
export const toToolChoice = (provider, choice) => {
|
|
136
|
+
if (!choice) return undefined
|
|
137
|
+
|
|
138
|
+
switch (provider) {
|
|
139
|
+
case 'anthropic':
|
|
140
|
+
if (choice === 'auto') return { type: 'auto' }
|
|
141
|
+
if (choice === 'required') return { type: 'any' }
|
|
142
|
+
if (choice === 'none') return { type: 'none' }
|
|
143
|
+
if (typeof choice === 'string') return { type: 'tool', name: choice }
|
|
144
|
+
return choice
|
|
145
|
+
|
|
146
|
+
case 'openai':
|
|
147
|
+
if (choice === 'auto') return 'auto'
|
|
148
|
+
if (choice === 'required') return 'required'
|
|
149
|
+
if (choice === 'none') return 'none'
|
|
150
|
+
if (typeof choice === 'string') return { type: 'function', function: { name: choice } }
|
|
151
|
+
return choice
|
|
152
|
+
|
|
153
|
+
case 'cerebras':
|
|
154
|
+
if (choice === 'auto') return 'auto'
|
|
155
|
+
if (choice === 'required') return 'required'
|
|
156
|
+
if (choice === 'none') return 'none'
|
|
157
|
+
if (typeof choice === 'string') return { type: 'function', function: { name: choice } }
|
|
158
|
+
return choice
|
|
159
|
+
|
|
160
|
+
case 'mistral':
|
|
161
|
+
if (choice === 'auto') return 'auto'
|
|
162
|
+
if (choice === 'required') return 'any'
|
|
163
|
+
if (choice === 'none') return 'none'
|
|
164
|
+
if (typeof choice === 'string') return { type: 'function', function: { name: choice } }
|
|
165
|
+
return choice
|
|
166
|
+
|
|
167
|
+
case 'gemini':
|
|
168
|
+
// Gemini uses toolConfig.functionCallingConfig
|
|
169
|
+
if (choice === 'auto') return { functionCallingConfig: { mode: 'AUTO' } }
|
|
170
|
+
if (choice === 'required') return { functionCallingConfig: { mode: 'ANY' } }
|
|
171
|
+
if (choice === 'none') return { functionCallingConfig: { mode: 'NONE' } }
|
|
172
|
+
if (typeof choice === 'string') {
|
|
173
|
+
return {
|
|
174
|
+
functionCallingConfig: {
|
|
175
|
+
mode: 'ANY',
|
|
176
|
+
allowedFunctionNames: [choice]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return choice
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
return choice
|
|
184
|
+
}
|
|
185
|
+
}
|