mohdel 3.6.1 → 3.7.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/js/chatgpt/index.js +2 -1
- package/js/session/adapters/_pricing.js +31 -2
- package/js/session/adapters/embedding/cohere.js +2 -2
- package/js/session/adapters/embedding/gemini.js +2 -2
- package/js/session/adapters/embedding/openai_compatible.js +2 -2
- package/js/session/adapters/transcription/openai_compatible.js +2 -2
- package/package.json +2 -2
- package/src/cli/ask.js +7 -3
- package/src/cli/model.js +6 -3
- package/src/lib/catalog-review.js +3 -0
- package/src/lib/providers.js +35 -0
package/js/chatgpt/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { hostname } from 'node:os'
|
|
|
4
4
|
import { createRemoteJWKSet, jwtVerify } from 'jose'
|
|
5
5
|
import { defaultDirectory, readStore, withStore } from './store.js'
|
|
6
6
|
import { discoverModels, visibleModels } from './models.js'
|
|
7
|
+
import providers from '../../src/lib/providers.js'
|
|
7
8
|
|
|
8
9
|
const ISSUER = 'https://auth.openai.com'
|
|
9
10
|
const RESOURCE = 'https://api.openai.com/v1'
|
|
@@ -15,7 +16,7 @@ const random = () => randomBytes(32).toString('base64url')
|
|
|
15
16
|
const REFRESH_MARGIN = 30000
|
|
16
17
|
// Every registration made before names were stored was sent this hint.
|
|
17
18
|
const UNNAMED_REGISTRATION = 'Mohdel'
|
|
18
|
-
export const usageURL =
|
|
19
|
+
export const usageURL = providers.chatgpt.billing.usage
|
|
19
20
|
|
|
20
21
|
async function jsonRequest (fetcher, url, options = {}) {
|
|
21
22
|
let response
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { setCatalog, specFor } from './_catalog.js'
|
|
13
|
+
import { providerOf } from '#core/model-id.js'
|
|
14
|
+
import providers from '../../../src/lib/providers.js'
|
|
15
|
+
|
|
16
|
+
// `plan` and `capacity` calls have no per-call API invoice, so `cost`, which is
|
|
17
|
+
// API USD, is 0 whatever the catalog prices. `fake` and `echo` are adapters,
|
|
18
|
+
// not providers: they carry no declaration and price from the catalog.
|
|
19
|
+
function unmetered (model) {
|
|
20
|
+
const def = providers[providerOf(model)]
|
|
21
|
+
return def !== undefined && def.billing.kind !== 'metered'
|
|
22
|
+
}
|
|
13
23
|
|
|
14
24
|
/**
|
|
15
25
|
* Pure cost computation from spec + usage.
|
|
@@ -112,11 +122,30 @@ function resolveTier (price, tokens) {
|
|
|
112
122
|
* @returns {number}
|
|
113
123
|
*/
|
|
114
124
|
export function costFor (envelope, usage) {
|
|
115
|
-
|
|
116
|
-
if (envelope.model.startsWith('chatgpt/')) return 0
|
|
125
|
+
if (unmetered(envelope.model)) return 0
|
|
117
126
|
return computeCost(specFor(envelope), usage)
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
/**
|
|
130
|
+
* @param {{model: string}} envelope
|
|
131
|
+
* @param {any} spec
|
|
132
|
+
* @param {{durationSeconds?: number | null, inputTokens?: number, outputTokens?: number}} usage
|
|
133
|
+
* @returns {number}
|
|
134
|
+
*/
|
|
135
|
+
export function transcriptionCostFor (envelope, spec, usage) {
|
|
136
|
+
return unmetered(envelope.model) ? 0 : computeTranscriptionCost(spec, usage)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {{model: string}} envelope
|
|
141
|
+
* @param {any} spec
|
|
142
|
+
* @param {{inputTokens?: number}} usage
|
|
143
|
+
* @returns {number}
|
|
144
|
+
*/
|
|
145
|
+
export function embeddingCostFor (envelope, spec, usage) {
|
|
146
|
+
return unmetered(envelope.model) ? 0 : computeEmbeddingCost(spec, usage)
|
|
147
|
+
}
|
|
148
|
+
|
|
120
149
|
/**
|
|
121
150
|
* Cost of a transcription call.
|
|
122
151
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { getSpec } from '../_catalog.js'
|
|
14
14
|
import { classifyProviderError, fromHttpStatus, typedError } from '../_errors.js'
|
|
15
|
-
import {
|
|
15
|
+
import { embeddingCostFor } from '../_pricing.js'
|
|
16
16
|
import { catalogKey, bareOf } from '#core/model-id.js'
|
|
17
17
|
import { checkBatch, checkDimensions, resolveInputType, widthOf } from './_shared.js'
|
|
18
18
|
|
|
@@ -85,7 +85,7 @@ export async function cohereEmbedding (envelope, deps = {}) {
|
|
|
85
85
|
dimensions: widthOf(vectors),
|
|
86
86
|
inputType,
|
|
87
87
|
inputTokens,
|
|
88
|
-
cost:
|
|
88
|
+
cost: embeddingCostFor(envelope, spec, { inputTokens }),
|
|
89
89
|
timestamps: { start, first: end, end }
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { getSpec } from '../_catalog.js'
|
|
15
15
|
import { classifyProviderError, fromHttpStatus, typedError } from '../_errors.js'
|
|
16
|
-
import {
|
|
16
|
+
import { embeddingCostFor } from '../_pricing.js'
|
|
17
17
|
import { catalogKey, bareOf } from '#core/model-id.js'
|
|
18
18
|
import { checkBatch, checkDimensions, resolveInputType, widthOf } from './_shared.js'
|
|
19
19
|
|
|
@@ -86,7 +86,7 @@ export async function geminiEmbedding (envelope, deps = {}) {
|
|
|
86
86
|
dimensions: widthOf(vectors),
|
|
87
87
|
inputType: taskType,
|
|
88
88
|
inputTokens,
|
|
89
|
-
cost:
|
|
89
|
+
cost: embeddingCostFor(envelope, spec, { inputTokens }),
|
|
90
90
|
timestamps: { start, first: end, end }
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { getSpec } from '../_catalog.js'
|
|
13
13
|
import { classifyProviderError, fromHttpStatus, typedError } from '../_errors.js'
|
|
14
|
-
import {
|
|
14
|
+
import { embeddingCostFor } from '../_pricing.js'
|
|
15
15
|
import { catalogKey, bareOf } from '#core/model-id.js'
|
|
16
16
|
import { checkBatch, checkDimensions, resolveInputType, widthOf } from './_shared.js'
|
|
17
17
|
|
|
@@ -85,7 +85,7 @@ export function createEmbeddingAdapter ({ baseURL, dimensionsField = 'dimensions
|
|
|
85
85
|
dimensions: widthOf(vectors),
|
|
86
86
|
inputType,
|
|
87
87
|
inputTokens,
|
|
88
|
-
cost:
|
|
88
|
+
cost: embeddingCostFor(envelope, spec, { inputTokens }),
|
|
89
89
|
timestamps: { start, first: end, end }
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -23,7 +23,7 @@ import { basename } from 'node:path'
|
|
|
23
23
|
import { getSpec } from '../_catalog.js'
|
|
24
24
|
import { classifyProviderError, fromHttpStatus, typedError } from '../_errors.js'
|
|
25
25
|
import { dataUriPayload, isTrustedMedia, mediaScheme, readLocalMedia } from '../_media.js'
|
|
26
|
-
import {
|
|
26
|
+
import { transcriptionCostFor } from '../_pricing.js'
|
|
27
27
|
import { catalogKey, bareOf } from '#core/model-id.js'
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -67,7 +67,7 @@ export function createTranscriptionAdapter ({ baseURL, responseFormat }) {
|
|
|
67
67
|
const body = await res.json()
|
|
68
68
|
const durationSeconds = extractDuration(body)
|
|
69
69
|
const tokens = extractTokens(body)
|
|
70
|
-
const cost =
|
|
70
|
+
const cost = transcriptionCostFor(envelope, spec, { durationSeconds, ...tokens })
|
|
71
71
|
|
|
72
72
|
const end = String(process.hrtime.bigint())
|
|
73
73
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
145
145
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
146
146
|
"chalk": "^6.0.1",
|
|
147
|
-
"mohdel-thin-gate-linux-x64-gnu": "3.
|
|
147
|
+
"mohdel-thin-gate-linux-x64-gnu": "3.7.0"
|
|
148
148
|
},
|
|
149
149
|
"dependencies": {
|
|
150
150
|
"@anthropic-ai/sdk": "^0.129.0",
|
package/src/cli/ask.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import mohdel, { silent } from '../lib/index.js'
|
|
2
2
|
import { getConfig, loadDefaultEnv } from '../lib/common.js'
|
|
3
|
+
import providerDefs, { billingOf } from '../lib/providers.js'
|
|
3
4
|
|
|
4
5
|
const noop = () => {}
|
|
5
6
|
|
|
@@ -47,8 +48,10 @@ export const hintsForError = (err, modelId) => {
|
|
|
47
48
|
else hints.push('→ run: mo # interactive provider/key setup')
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
// The id is what the user typed, so its provider may not exist.
|
|
52
|
+
const usage = providerDefs[provider]?.billing.usage
|
|
53
|
+
if (usage && /RATE_LIMIT|QUOTA_EXHAUSTED|429|usage limit/i.test(`${err?.type || ''} ${both}`)) {
|
|
54
|
+
hints.push(`→ manage ${providerDefs[provider].billing.label} usage: ${usage}`)
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
if (/deprecated/i.test(both) && /replacement/i.test(both)) {
|
|
@@ -223,7 +226,8 @@ Examples:
|
|
|
223
226
|
if (tokens.inputTokens) summary.push(`${tokens.inputTokens} in`)
|
|
224
227
|
if (tokens.outputTokens) summary.push(`${tokens.outputTokens} out`)
|
|
225
228
|
if (tokens.thinkingTokens) summary.push(`${tokens.thinkingTokens} think`)
|
|
226
|
-
|
|
229
|
+
const billing = billingOf(model.id)
|
|
230
|
+
if (billing.kind !== 'metered') summary.push(`not metered · ${billing.label}${billing.usage ? ` — manage usage: ${billing.usage}` : ''}`)
|
|
227
231
|
else if (tokens.cost != null) summary.push(`$${tokens.cost.toFixed(4)}`)
|
|
228
232
|
if (tokens.speed) {
|
|
229
233
|
const served = tokens.servedSpeed
|
package/src/cli/model.js
CHANGED
|
@@ -731,11 +731,11 @@ Alibaba's Qwen. Routing follows the provider — see "mo provider --help".`)
|
|
|
731
731
|
|
|
732
732
|
// The provider API returns ids, never prices — curated entries land unpriced.
|
|
733
733
|
function printCurateNext (providerName) {
|
|
734
|
-
|
|
735
|
-
|
|
734
|
+
const def = providerDefs[providerName]
|
|
735
|
+
if (def.billing.kind !== 'metered') {
|
|
736
|
+
console.log(`${providerName} models are ready. Use mo ask ${providerName}/<model-id> "your prompt". Calls are not metered: they draw on your ${def.billing.label}.`)
|
|
736
737
|
return
|
|
737
738
|
}
|
|
738
|
-
const def = providerDefs[providerName]
|
|
739
739
|
if (def?.pricesFromApi) {
|
|
740
740
|
console.log(`\n${meta(`${providerName} publishes prices in its model list — the entries are complete.`)}`)
|
|
741
741
|
console.log(`${meta('Check them:')} mo ls ${meta('│')} mo check`)
|
|
@@ -779,6 +779,9 @@ const SINGLE_DIMENSION_PRICES = [
|
|
|
779
779
|
]
|
|
780
780
|
|
|
781
781
|
function formatPrice (info) {
|
|
782
|
+
// An entry naming an unknown provider is `mo check`'s to report; it lists with its prices.
|
|
783
|
+
const billing = providerDefs[info.provider]?.billing
|
|
784
|
+
if (billing && billing.kind !== 'metered') return meta(`not metered · ${billing.label}`)
|
|
782
785
|
const inp = resolvePrice(info.inputPrice)
|
|
783
786
|
const out = resolvePrice(info.outputPrice)
|
|
784
787
|
if (inp || out) return price(`$${inp}`) + meta('/') + price(`$${out}`)
|
|
@@ -73,6 +73,9 @@ export const reviewEntry = (key, spec, catalog, { strict = false, local = null }
|
|
|
73
73
|
if (spec.provider && spec.provider !== keyProvider) {
|
|
74
74
|
errors.push(`${key}: spec.provider '${spec.provider}' doesn't match key prefix '${keyProvider}'`)
|
|
75
75
|
}
|
|
76
|
+
if (providerConfig?.billing.kind === 'capacity' && Object.keys(spec).some(k => k.endsWith('Price'))) {
|
|
77
|
+
warnings.push(`${key}: prices are never charged — ${providerConfig.billing.label} calls report cost 0`)
|
|
78
|
+
}
|
|
76
79
|
if (providerConfig && spec.sdk && spec.sdk !== providerConfig.sdk) {
|
|
77
80
|
errors.push(`${key}: spec.sdk '${spec.sdk}' doesn't match provider sdk '${providerConfig.sdk}'`)
|
|
78
81
|
}
|
package/src/lib/providers.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { providerOf } from '#core/model-id.js'
|
|
2
|
+
|
|
1
3
|
const LOCAL_API_KEY_ENV = 'MOHDEL_LOCAL_API_SK'
|
|
2
4
|
|
|
3
5
|
// `contextSemantics` and `outputCapStrategy` are published facts about a
|
|
@@ -6,8 +8,13 @@ const LOCAL_API_KEY_ENV = 'MOHDEL_LOCAL_API_SK'
|
|
|
6
8
|
// own provider requests does not have to rediscover the behaviour one 400 at a
|
|
7
9
|
// time. Entries may override `outputCapStrategy` per model. See
|
|
8
10
|
// ARCHITECTURE.md > "The output budget is capped to the model's ceiling".
|
|
11
|
+
// `billing` is how a provider's calls are paid for. `metered`: API money,
|
|
12
|
+
// reported as `cost`. `plan`: a share of a subscription allowance, seen at
|
|
13
|
+
// `usage`. `capacity`: hardware run or rented at a flat rate. `cost` is 0 for
|
|
14
|
+
// the last two.
|
|
9
15
|
const providers = {
|
|
10
16
|
anthropic: {
|
|
17
|
+
billing: { kind: 'metered' },
|
|
11
18
|
sdk: 'anthropic',
|
|
12
19
|
apiKeyEnv: 'ANTHROPIC_API_SK',
|
|
13
20
|
createConfiguration: apiKey => ({ apiKey }),
|
|
@@ -20,6 +27,7 @@ const providers = {
|
|
|
20
27
|
outputCapStrategy: 'error'
|
|
21
28
|
},
|
|
22
29
|
cerebras: {
|
|
30
|
+
billing: { kind: 'metered' },
|
|
23
31
|
sdk: 'cerebras',
|
|
24
32
|
apiKeyEnv: 'CEREBRAS_API_SK',
|
|
25
33
|
createConfiguration: apiKey => ({ apiKey }),
|
|
@@ -32,6 +40,7 @@ const providers = {
|
|
|
32
40
|
outputCapStrategy: 'accept'
|
|
33
41
|
},
|
|
34
42
|
chatgpt: {
|
|
43
|
+
billing: { kind: 'plan', label: 'ChatGPT plan', usage: 'https://chatgpt.com/settings/usage' },
|
|
35
44
|
sdk: 'openai',
|
|
36
45
|
catalogClient: 'chatgpt',
|
|
37
46
|
refreshConfiguration: true,
|
|
@@ -47,6 +56,7 @@ const providers = {
|
|
|
47
56
|
outputCapStrategy: 'accept'
|
|
48
57
|
},
|
|
49
58
|
deepseek: {
|
|
59
|
+
billing: { kind: 'metered' },
|
|
50
60
|
sdk: 'openai',
|
|
51
61
|
api: 'chatCompletions',
|
|
52
62
|
apiKeyEnv: 'DEEPSEEK_API_SK',
|
|
@@ -61,6 +71,7 @@ const providers = {
|
|
|
61
71
|
outputCapStrategy: 'accept'
|
|
62
72
|
},
|
|
63
73
|
fireworks: {
|
|
74
|
+
billing: { kind: 'metered' },
|
|
64
75
|
sdk: 'fireworks',
|
|
65
76
|
apiKeyEnv: 'FIREWORKS_API_SK',
|
|
66
77
|
baseURL: 'https://api.fireworks.ai/inference/v1',
|
|
@@ -74,6 +85,7 @@ const providers = {
|
|
|
74
85
|
outputCapStrategy: 'accept'
|
|
75
86
|
},
|
|
76
87
|
gemini: {
|
|
88
|
+
billing: { kind: 'metered' },
|
|
77
89
|
sdk: 'gemini',
|
|
78
90
|
apiKeyEnv: 'GEMINI_API_SK',
|
|
79
91
|
createConfiguration: apiKey => ({ apiKey }),
|
|
@@ -86,6 +98,7 @@ const providers = {
|
|
|
86
98
|
outputCapStrategy: 'accept'
|
|
87
99
|
},
|
|
88
100
|
groq: {
|
|
101
|
+
billing: { kind: 'metered' },
|
|
89
102
|
sdk: 'groq',
|
|
90
103
|
apiKeyEnv: 'GROQ_API_SK',
|
|
91
104
|
createConfiguration: apiKey => ({ apiKey }),
|
|
@@ -96,6 +109,7 @@ const providers = {
|
|
|
96
109
|
}
|
|
97
110
|
},
|
|
98
111
|
local: {
|
|
112
|
+
billing: { kind: 'capacity', label: 'local server' },
|
|
99
113
|
sdk: 'openai',
|
|
100
114
|
api: 'chatCompletions',
|
|
101
115
|
catalog: false,
|
|
@@ -105,6 +119,7 @@ const providers = {
|
|
|
105
119
|
outputCapStrategy: 'accept'
|
|
106
120
|
},
|
|
107
121
|
meta: {
|
|
122
|
+
billing: { kind: 'metered' },
|
|
108
123
|
sdk: 'openai',
|
|
109
124
|
apiKeyEnv: 'META_API_SK',
|
|
110
125
|
baseURL: 'https://api.meta.ai/v1',
|
|
@@ -117,6 +132,7 @@ const providers = {
|
|
|
117
132
|
outputCapStrategy: 'accept'
|
|
118
133
|
},
|
|
119
134
|
cohere: {
|
|
135
|
+
billing: { kind: 'metered' },
|
|
120
136
|
sdk: 'cohere',
|
|
121
137
|
api: 'embeddings',
|
|
122
138
|
apiKeyEnv: 'COHERE_API_SK',
|
|
@@ -129,6 +145,7 @@ const providers = {
|
|
|
129
145
|
}
|
|
130
146
|
},
|
|
131
147
|
mistral: {
|
|
148
|
+
billing: { kind: 'metered' },
|
|
132
149
|
sdk: 'openai',
|
|
133
150
|
api: 'chatCompletions',
|
|
134
151
|
apiKeyEnv: 'MISTRAL_API_SK',
|
|
@@ -141,6 +158,7 @@ const providers = {
|
|
|
141
158
|
}
|
|
142
159
|
},
|
|
143
160
|
novita: {
|
|
161
|
+
billing: { kind: 'metered' },
|
|
144
162
|
sdk: 'openai',
|
|
145
163
|
api: 'chatCompletions',
|
|
146
164
|
imageHandler: 'novita',
|
|
@@ -157,6 +175,7 @@ const providers = {
|
|
|
157
175
|
outputCapStrategy: 'error'
|
|
158
176
|
},
|
|
159
177
|
openai: {
|
|
178
|
+
billing: { kind: 'metered' },
|
|
160
179
|
sdk: 'openai',
|
|
161
180
|
apiKeyEnv: 'OPENAI_API_SK',
|
|
162
181
|
createConfiguration: apiKey => ({ apiKey }),
|
|
@@ -169,6 +188,7 @@ const providers = {
|
|
|
169
188
|
outputCapStrategy: 'accept'
|
|
170
189
|
},
|
|
171
190
|
openrouter: {
|
|
191
|
+
billing: { kind: 'metered' },
|
|
172
192
|
sdk: 'openrouter',
|
|
173
193
|
apiKeyEnv: 'OPENROUTER_API_SK',
|
|
174
194
|
baseURL: 'https://openrouter.ai/api/v1',
|
|
@@ -184,6 +204,7 @@ const providers = {
|
|
|
184
204
|
}
|
|
185
205
|
},
|
|
186
206
|
qwen: {
|
|
207
|
+
billing: { kind: 'metered' },
|
|
187
208
|
sdk: 'openai',
|
|
188
209
|
api: 'chatCompletions',
|
|
189
210
|
apiKeyEnv: 'QWEN_API_SK',
|
|
@@ -198,6 +219,7 @@ const providers = {
|
|
|
198
219
|
outputCapStrategy: 'accept'
|
|
199
220
|
},
|
|
200
221
|
xai: {
|
|
222
|
+
billing: { kind: 'metered' },
|
|
201
223
|
sdk: 'openai',
|
|
202
224
|
apiKeyEnv: 'XAI_API_SK',
|
|
203
225
|
baseURL: 'https://api.x.ai/v1',
|
|
@@ -211,6 +233,7 @@ const providers = {
|
|
|
211
233
|
outputCapStrategy: 'accept'
|
|
212
234
|
},
|
|
213
235
|
xiaomi: {
|
|
236
|
+
billing: { kind: 'metered' },
|
|
214
237
|
sdk: 'openai',
|
|
215
238
|
api: 'chatCompletions',
|
|
216
239
|
apiKeyEnv: 'XIAOMI_API_SK',
|
|
@@ -223,4 +246,16 @@ const providers = {
|
|
|
223
246
|
|
|
224
247
|
Object.freeze(providers)
|
|
225
248
|
|
|
249
|
+
/**
|
|
250
|
+
* How calls to `modelId`'s provider are paid for.
|
|
251
|
+
* @param {string} modelId
|
|
252
|
+
* @returns {{kind: 'metered' | 'plan' | 'capacity', label?: string, usage?: string}}
|
|
253
|
+
*/
|
|
254
|
+
export function billingOf (modelId) {
|
|
255
|
+
const provider = providerOf(modelId)
|
|
256
|
+
const def = providers[provider]
|
|
257
|
+
if (!def) throw new Error(`Unknown provider '${provider}' in model id '${modelId}'`)
|
|
258
|
+
return { ...def.billing }
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
export default providers
|