typeclaw 0.32.1 → 0.34.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/auth.schema.json +66 -0
- package/cron.schema.json +26 -2
- package/package.json +1 -1
- package/secrets.schema.json +66 -0
- package/src/agent/index.ts +7 -3
- package/src/agent/session-origin.ts +17 -0
- package/src/agent/subagent-completion-reminder.ts +14 -1
- package/src/agent/subagent-drain.ts +2 -0
- package/src/agent/subagents.ts +21 -7
- package/src/agent/tools/channel-disengage.ts +66 -0
- package/src/agent/tools/channel-log.ts +3 -2
- package/src/agent/tools/spawn-subagent.ts +25 -5
- package/src/agent/tools/subagent-output.ts +13 -1
- package/src/bundled-plugins/guard/policies/managed-config.ts +1 -1
- package/src/bundled-plugins/memory/memory-logger.ts +7 -0
- package/src/bundled-plugins/researcher/researcher.ts +14 -11
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/adapters/line-channel-resolver.ts +129 -0
- package/src/channels/adapters/line-classify.ts +80 -0
- package/src/channels/adapters/line-format.ts +11 -0
- package/src/channels/adapters/line.ts +350 -0
- package/src/channels/engagement.ts +4 -2
- package/src/channels/manager.ts +65 -6
- package/src/channels/router.ts +186 -41
- package/src/channels/schema.ts +6 -1
- package/src/cli/channel.ts +112 -1
- package/src/cli/cron.ts +22 -4
- package/src/cli/init.ts +267 -82
- package/src/cli/model.ts +5 -1
- package/src/cli/oauth-callbacks.ts +5 -4
- package/src/cli/provider.ts +41 -10
- package/src/config/providers.ts +366 -7
- package/src/cron/consumer.ts +33 -0
- package/src/cron/count-state.ts +208 -0
- package/src/cron/index.ts +4 -17
- package/src/cron/list.ts +24 -6
- package/src/cron/scheduler.ts +84 -9
- package/src/cron/schema.ts +100 -13
- package/src/doctor/channel-checks.ts +28 -0
- package/src/hostd/daemon.ts +14 -6
- package/src/hostd/protocol.ts +6 -2
- package/src/init/gitignore.ts +1 -1
- package/src/init/index.ts +36 -3
- package/src/init/line-auth.ts +98 -0
- package/src/init/models-dev.ts +3 -0
- package/src/init/run-owner-claim.ts +1 -0
- package/src/init/validate-api-key.ts +15 -0
- package/src/inspect/label.ts +1 -0
- package/src/permissions/match-rule.ts +28 -12
- package/src/permissions/resolve.ts +8 -1
- package/src/role-claim/match-rule.ts +5 -1
- package/src/run/index.ts +41 -4
- package/src/secrets/line-store.ts +112 -0
- package/src/secrets/oauth-xai.ts +342 -0
- package/src/secrets/schema.ts +25 -0
- package/src/secrets/storage.ts +2 -0
- package/src/server/index.ts +17 -4
- package/src/shared/protocol.ts +4 -1
- package/src/skills/typeclaw-channel-line/SKILL.md +46 -0
- package/src/skills/typeclaw-channels/SKILL.md +153 -0
- package/src/skills/typeclaw-config/SKILL.md +54 -184
- package/src/skills/typeclaw-config/references/dockerfile.md +66 -0
- package/src/skills/typeclaw-cron/SKILL.md +68 -14
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/typeclaw.schema.json +185 -3
package/src/cli/provider.ts
CHANGED
|
@@ -2,10 +2,16 @@ import { cancel, intro, isCancel, log, password, select } from '@clack/prompts'
|
|
|
2
2
|
import { defineCommand } from 'citty'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
KNOWN_PROVIDER_VENDORS,
|
|
5
6
|
KNOWN_PROVIDERS,
|
|
7
|
+
listKnownProviderVendorIds,
|
|
8
|
+
providerIdsForVendor,
|
|
6
9
|
supportsApiKey as providerSupportsApiKey,
|
|
7
10
|
supportsOAuth as providerSupportsOAuth,
|
|
11
|
+
variantHint,
|
|
12
|
+
variantLabel,
|
|
8
13
|
type KnownProviderId,
|
|
14
|
+
type KnownProviderVendorId,
|
|
9
15
|
} from '@/config/providers'
|
|
10
16
|
import {
|
|
11
17
|
addProvider,
|
|
@@ -270,15 +276,40 @@ function validateKnownProvider(input: string): KnownProviderId {
|
|
|
270
276
|
|
|
271
277
|
async function resolveProviderForAdd(input: string | undefined): Promise<KnownProviderId> {
|
|
272
278
|
if (input !== undefined) return validateKnownProvider(input)
|
|
273
|
-
const
|
|
274
|
-
|
|
279
|
+
const vendorId = await pickVendorToAdd()
|
|
280
|
+
return await pickVariantToAdd(vendorId)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function pickVendorToAdd(): Promise<KnownProviderVendorId> {
|
|
284
|
+
const vendorIds = listKnownProviderVendorIds()
|
|
285
|
+
const choice = await select<KnownProviderVendorId>({
|
|
275
286
|
message: 'Pick a provider to add',
|
|
276
|
-
options:
|
|
287
|
+
options: vendorIds.map((id) => ({
|
|
277
288
|
value: id,
|
|
278
|
-
label:
|
|
279
|
-
hint:
|
|
289
|
+
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
290
|
+
hint: vendorAuthHint(id),
|
|
280
291
|
})),
|
|
281
|
-
initialValue:
|
|
292
|
+
initialValue: vendorIds[0],
|
|
293
|
+
})
|
|
294
|
+
if (isCancel(choice)) {
|
|
295
|
+
cancel('Aborted.')
|
|
296
|
+
process.exit(0)
|
|
297
|
+
}
|
|
298
|
+
return choice
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function pickVariantToAdd(vendorId: KnownProviderVendorId): Promise<KnownProviderId> {
|
|
302
|
+
const variants = providerIdsForVendor(vendorId)
|
|
303
|
+
if (variants.length === 1) return variants[0]!
|
|
304
|
+
const choice = await select<KnownProviderId>({
|
|
305
|
+
message: `Pick a ${KNOWN_PROVIDER_VENDORS[vendorId].name} option`,
|
|
306
|
+
options: variants.map((id) => {
|
|
307
|
+
const hint = variantHint(vendorId, id)
|
|
308
|
+
return hint !== undefined
|
|
309
|
+
? { value: id, label: variantLabel(vendorId, id), hint }
|
|
310
|
+
: { value: id, label: variantLabel(vendorId, id) }
|
|
311
|
+
}),
|
|
312
|
+
initialValue: variants[0],
|
|
282
313
|
})
|
|
283
314
|
if (isCancel(choice)) {
|
|
284
315
|
cancel('Aborted.')
|
|
@@ -378,10 +409,10 @@ async function runOAuthLogin(cwd: string, providerId: KnownProviderId): Promise<
|
|
|
378
409
|
}
|
|
379
410
|
}
|
|
380
411
|
|
|
381
|
-
function
|
|
382
|
-
const
|
|
383
|
-
const apiKey = providerSupportsApiKey(
|
|
384
|
-
const oauth = providerSupportsOAuth(
|
|
412
|
+
function vendorAuthHint(vendorId: KnownProviderVendorId): string {
|
|
413
|
+
const providers = providerIdsForVendor(vendorId)
|
|
414
|
+
const apiKey = providers.some((id) => providerSupportsApiKey(KNOWN_PROVIDERS[id]))
|
|
415
|
+
const oauth = providers.some((id) => providerSupportsOAuth(KNOWN_PROVIDERS[id]))
|
|
385
416
|
if (apiKey && oauth) return 'API key or OAuth'
|
|
386
417
|
if (oauth) return 'OAuth only'
|
|
387
418
|
return 'API key'
|
package/src/config/providers.ts
CHANGED
|
@@ -41,6 +41,17 @@ type KnownProvider = {
|
|
|
41
41
|
// e.g. `OPENAI_API_KEY`). For `oauth` providers, `oauthProviderId` MUST match
|
|
42
42
|
// a pi-ai OAuth provider id exactly, otherwise `authStorage.login()` will
|
|
43
43
|
// throw "Unknown OAuth provider".
|
|
44
|
+
//
|
|
45
|
+
// Granularity rule (split vs merge): a provider id is the runtime API surface,
|
|
46
|
+
// not the brand. Different API call => different provider id; same API call =>
|
|
47
|
+
// same provider id. "Same API call" means same endpoint + same wire transport.
|
|
48
|
+
// So `anthropic` is ONE id because api-key and oauth hit the same
|
|
49
|
+
// /v1/messages endpoint (only the auth header differs), while `openai` /
|
|
50
|
+
// `openai-codex` and `zai` / `zai-coding` are SEPARATE ids because each pair
|
|
51
|
+
// targets different endpoints (and env vars). The user-facing brand grouping
|
|
52
|
+
// lives in `KNOWN_PROVIDER_VENDORS` below — keep it out of this decision.
|
|
53
|
+
// Renaming an id is a breaking change to secrets.json keys and typeclaw.json
|
|
54
|
+
// model refs; only do it behind a migration in a dedicated major-version PR.
|
|
44
55
|
export const KNOWN_PROVIDERS = {
|
|
45
56
|
openai: {
|
|
46
57
|
id: 'openai',
|
|
@@ -119,13 +130,12 @@ export const KNOWN_PROVIDERS = {
|
|
|
119
130
|
// these ids work end-to-end as long as the Codex backend itself accepts
|
|
120
131
|
// them, which it does for ChatGPT Plus/Pro accounts as of 2026-05-10.
|
|
121
132
|
//
|
|
122
|
-
// Position-load-bearing: must stay adjacent to `openai`.
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// prevent.
|
|
133
|
+
// Position-load-bearing: must stay adjacent to `openai`. `provider --help`'s
|
|
134
|
+
// `id | id | ...` listing and the generated JSON schema's model-ref enum
|
|
135
|
+
// derive their ordering from Object.keys() iteration on this literal, so
|
|
136
|
+
// alphabetizing the registry would scatter `openai-codex` after `fireworks`.
|
|
137
|
+
// (The init wizard's picker no longer depends on this order — it groups by
|
|
138
|
+
// `KNOWN_PROVIDER_VENDORS` below.)
|
|
129
139
|
'openai-codex': {
|
|
130
140
|
id: 'openai-codex',
|
|
131
141
|
name: 'OpenAI Codex (ChatGPT Plus/Pro)',
|
|
@@ -453,10 +463,359 @@ export const KNOWN_PROVIDERS = {
|
|
|
453
463
|
},
|
|
454
464
|
},
|
|
455
465
|
},
|
|
466
|
+
// xAI (Grok). The native developer API at api.x.ai/v1 is OpenAI-compatible
|
|
467
|
+
// (Bearer auth + /chat/completions shape), so models go through pi-ai's
|
|
468
|
+
// `openai-completions` adapter with a custom baseUrl — same trick as
|
|
469
|
+
// Fireworks and Z.AI. This is a DUAL-AUTH provider like `anthropic`:
|
|
470
|
+
// * api-key — XAI_API_KEY (the standard xAI env var), a plain Bearer token.
|
|
471
|
+
// * oauth — Grok subscription login via xAI's OIDC server (auth.x.ai),
|
|
472
|
+
// authorization-code + PKCE. pi-ai ships no built-in xAI OAuth provider,
|
|
473
|
+
// so `oauthProviderId: 'xai'` resolves to the custom provider registered
|
|
474
|
+
// in src/secrets/oauth-xai.ts (registered from createSecretsStoreForAgent
|
|
475
|
+
// so both init-login and runtime-refresh see it). The dual-auth runtime
|
|
476
|
+
// rule in src/agent/auth.ts applies: an OAuth credential on disk wins over
|
|
477
|
+
// XAI_API_KEY in .env — remove it (`typeclaw provider remove xai`) to fall
|
|
478
|
+
// back to the key.
|
|
479
|
+
//
|
|
480
|
+
// Costs and context windows mirror docs.x.ai/developers/models and the raw
|
|
481
|
+
// /v1/models price fields as of 2026-06-08 (xAI quotes prices in cents per
|
|
482
|
+
// 100M tokens; e.g. grok-4.3 prompt 12500 = $1.25/1M). grok-4.3 is the
|
|
483
|
+
// flagship default; grok-build-0.1 is the coding-tuned model. The
|
|
484
|
+
// grok-4.20-0309 snapshots are pinned weights for reproducible runs.
|
|
485
|
+
//
|
|
486
|
+
// The earlier grok-4 / grok-4-fast / grok-code-fast-1 ids were RETIRED on
|
|
487
|
+
// 2026-05-15 — they still resolve but silently redirect (and bill) at the
|
|
488
|
+
// grok-4.3 / grok-build-0.1 rates, so they are intentionally NOT listed.
|
|
489
|
+
//
|
|
490
|
+
// cacheWrite is 0: xAI publishes no cache-write price (caching is implicit,
|
|
491
|
+
// billed only at the cacheRead rate). Models 1-4 also carry a long-context
|
|
492
|
+
// tier (2x rates above a 200k-token request); pi-ai's Model shape can't
|
|
493
|
+
// express tiered pricing, so the standard rate is used and the breakpoint is
|
|
494
|
+
// noted here. When refreshing, rerun `scripts/generate-schema.ts`.
|
|
495
|
+
xai: {
|
|
496
|
+
id: 'xai',
|
|
497
|
+
name: 'xAI (Grok)',
|
|
498
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
499
|
+
auth: ['api-key', 'oauth'],
|
|
500
|
+
apiKeyEnv: 'XAI_API_KEY',
|
|
501
|
+
oauthProviderId: 'xai',
|
|
502
|
+
models: {
|
|
503
|
+
'grok-4.3': {
|
|
504
|
+
id: 'grok-4.3',
|
|
505
|
+
name: 'Grok 4.3',
|
|
506
|
+
api: 'openai-completions',
|
|
507
|
+
provider: 'xai',
|
|
508
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
509
|
+
reasoning: true,
|
|
510
|
+
input: ['text', 'image'],
|
|
511
|
+
cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
512
|
+
contextWindow: 1000000,
|
|
513
|
+
maxTokens: 64000,
|
|
514
|
+
},
|
|
515
|
+
'grok-4.20-0309-reasoning': {
|
|
516
|
+
id: 'grok-4.20-0309-reasoning',
|
|
517
|
+
name: 'Grok 4.20 (Reasoning)',
|
|
518
|
+
api: 'openai-completions',
|
|
519
|
+
provider: 'xai',
|
|
520
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
521
|
+
reasoning: true,
|
|
522
|
+
input: ['text', 'image'],
|
|
523
|
+
cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
524
|
+
contextWindow: 1000000,
|
|
525
|
+
maxTokens: 64000,
|
|
526
|
+
},
|
|
527
|
+
'grok-4.20-0309-non-reasoning': {
|
|
528
|
+
id: 'grok-4.20-0309-non-reasoning',
|
|
529
|
+
name: 'Grok 4.20 (Non-Reasoning)',
|
|
530
|
+
api: 'openai-completions',
|
|
531
|
+
provider: 'xai',
|
|
532
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
533
|
+
reasoning: false,
|
|
534
|
+
input: ['text', 'image'],
|
|
535
|
+
cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
536
|
+
contextWindow: 1000000,
|
|
537
|
+
maxTokens: 64000,
|
|
538
|
+
},
|
|
539
|
+
'grok-build-0.1': {
|
|
540
|
+
id: 'grok-build-0.1',
|
|
541
|
+
name: 'Grok Build 0.1',
|
|
542
|
+
api: 'openai-completions',
|
|
543
|
+
provider: 'xai',
|
|
544
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
545
|
+
reasoning: true,
|
|
546
|
+
input: ['text', 'image'],
|
|
547
|
+
cost: { input: 1.0, output: 2.0, cacheRead: 0.2, cacheWrite: 0 },
|
|
548
|
+
contextWindow: 256000,
|
|
549
|
+
maxTokens: 64000,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
// MiniMax (minimax.io) pay-as-you-go API. OpenAI-compatible (Bearer auth +
|
|
554
|
+
// /chat/completions shape), so models go through pi-ai's `openai-completions`
|
|
555
|
+
// adapter with a custom baseUrl — same trick as Fireworks and Z.AI.
|
|
556
|
+
//
|
|
557
|
+
// Endpoint choice: the international endpoint (api.minimax.io) is the global
|
|
558
|
+
// surface; the China endpoint (api.minimaxi.com) is a regional alternative on
|
|
559
|
+
// the same protocol. We pin the international one here — operators who need the
|
|
560
|
+
// China gateway can front it with an OpenAI-compatible proxy.
|
|
561
|
+
//
|
|
562
|
+
// Model lineup mirrors the OpenAI-compatible model enum on
|
|
563
|
+
// platform.minimax.io as of 2026-06-08: MiniMax-M3 (flagship, 1M context,
|
|
564
|
+
// image input, controllable reasoning) plus the M2 reasoning series
|
|
565
|
+
// (204,800 context, reasoning always on, text-only). The `-highspeed`
|
|
566
|
+
// billing-tier variants and the native-only `M2-her` / `MiniMax-Text-01` /
|
|
567
|
+
// `abab*` models are intentionally omitted — the first are duplicate weights
|
|
568
|
+
// on a pricier surface, the rest don't serve the OpenAI-compatible
|
|
569
|
+
// /chat/completions route this adapter speaks. Costs are USD per 1M tokens
|
|
570
|
+
// from docs/guides/pricing-paygo (standard tier): M3 reflects the permanent
|
|
571
|
+
// 50%-off ≤512K-input rate (input 0.30 / output 1.20 / cacheRead 0.06; M3 has
|
|
572
|
+
// no separate cache-write rate). M2.7 caches at 0.06 read / 0.375 write; the
|
|
573
|
+
// M2.5/M2.1/M2 series at 0.03 read / 0.375 write.
|
|
574
|
+
//
|
|
575
|
+
// M3 tiered pricing is NOT modeled: this single cost record only encodes the
|
|
576
|
+
// ≤512K-input tier. The >512K tier (input 0.60 / output 2.40 / cacheRead 0.12)
|
|
577
|
+
// is a limited-availability surcharge that pi-ai's flat per-model cost shape
|
|
578
|
+
// can't represent, so long-context M3 sessions above 512K under-report cost.
|
|
579
|
+
minimax: {
|
|
580
|
+
id: 'minimax',
|
|
581
|
+
name: 'MiniMax',
|
|
582
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
583
|
+
auth: ['api-key'],
|
|
584
|
+
apiKeyEnv: 'MINIMAX_API_KEY',
|
|
585
|
+
oauthProviderId: null,
|
|
586
|
+
models: {
|
|
587
|
+
'MiniMax-M3': {
|
|
588
|
+
id: 'MiniMax-M3',
|
|
589
|
+
name: 'MiniMax M3',
|
|
590
|
+
api: 'openai-completions',
|
|
591
|
+
provider: 'minimax',
|
|
592
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
593
|
+
reasoning: true,
|
|
594
|
+
input: ['text', 'image'],
|
|
595
|
+
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
596
|
+
contextWindow: 1000000,
|
|
597
|
+
maxTokens: 524288,
|
|
598
|
+
},
|
|
599
|
+
'MiniMax-M2.7': {
|
|
600
|
+
id: 'MiniMax-M2.7',
|
|
601
|
+
name: 'MiniMax M2.7',
|
|
602
|
+
api: 'openai-completions',
|
|
603
|
+
provider: 'minimax',
|
|
604
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
605
|
+
reasoning: true,
|
|
606
|
+
input: ['text'],
|
|
607
|
+
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
|
|
608
|
+
contextWindow: 204800,
|
|
609
|
+
maxTokens: 204800,
|
|
610
|
+
},
|
|
611
|
+
'MiniMax-M2.5': {
|
|
612
|
+
id: 'MiniMax-M2.5',
|
|
613
|
+
name: 'MiniMax M2.5',
|
|
614
|
+
api: 'openai-completions',
|
|
615
|
+
provider: 'minimax',
|
|
616
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
617
|
+
reasoning: true,
|
|
618
|
+
input: ['text'],
|
|
619
|
+
cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
|
|
620
|
+
contextWindow: 204800,
|
|
621
|
+
maxTokens: 204800,
|
|
622
|
+
},
|
|
623
|
+
'MiniMax-M2.1': {
|
|
624
|
+
id: 'MiniMax-M2.1',
|
|
625
|
+
name: 'MiniMax M2.1',
|
|
626
|
+
api: 'openai-completions',
|
|
627
|
+
provider: 'minimax',
|
|
628
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
629
|
+
reasoning: true,
|
|
630
|
+
input: ['text'],
|
|
631
|
+
cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
|
|
632
|
+
contextWindow: 204800,
|
|
633
|
+
maxTokens: 204800,
|
|
634
|
+
},
|
|
635
|
+
'MiniMax-M2': {
|
|
636
|
+
id: 'MiniMax-M2',
|
|
637
|
+
name: 'MiniMax M2',
|
|
638
|
+
api: 'openai-completions',
|
|
639
|
+
provider: 'minimax',
|
|
640
|
+
baseUrl: 'https://api.minimax.io/v1',
|
|
641
|
+
reasoning: true,
|
|
642
|
+
input: ['text'],
|
|
643
|
+
cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.375 },
|
|
644
|
+
contextWindow: 204800,
|
|
645
|
+
maxTokens: 204800,
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
// DeepSeek (api.deepseek.com) pay-as-you-go API. OpenAI-compatible (Bearer
|
|
650
|
+
// auth + /chat/completions shape), so models go through pi-ai's
|
|
651
|
+
// `openai-completions` adapter with a custom baseUrl — same trick as
|
|
652
|
+
// Fireworks, Z.AI, and MiniMax. api-key only; DeepSeek ships no OAuth flow.
|
|
653
|
+
//
|
|
654
|
+
// baseUrl is bare `https://api.deepseek.com` (no `/v1` segment) — the SDK
|
|
655
|
+
// appends `/chat/completions`. This mirrors Anthropic's no-`/v1` convention,
|
|
656
|
+
// not the OpenAI/xAI `/v1` one; `validate-api-key.ts` probes
|
|
657
|
+
// `${baseUrl}/models` accordingly.
|
|
658
|
+
//
|
|
659
|
+
// Model lineup is the V4 generation as listed on api-docs.deepseek.com/quick_start/pricing
|
|
660
|
+
// as of 2026-06-08: deepseek-v4-flash (fast, cheap default) and
|
|
661
|
+
// deepseek-v4-pro (stronger). Both default to thinking/reasoning mode (it is
|
|
662
|
+
// toggleable upstream, but reasoning: true reflects the default). 1M context,
|
|
663
|
+
// 384K max output, text-only — DeepSeek's API exposes no image input. The
|
|
664
|
+
// legacy `deepseek-chat` / `deepseek-reasoner` aliases (deprecated 2026-07-24,
|
|
665
|
+
// they redirect into v4-flash's non-thinking/thinking modes) are intentionally
|
|
666
|
+
// omitted in favor of the canonical v4 ids.
|
|
667
|
+
//
|
|
668
|
+
// Costs are USD per 1M tokens (standard tier). DeepSeek prices input on a
|
|
669
|
+
// cache-miss/cache-hit split: `input` is the cache-miss rate, `cacheRead` is
|
|
670
|
+
// the cache-hit rate. There is no published cache-write surcharge, so
|
|
671
|
+
// cacheWrite is 0.
|
|
672
|
+
deepseek: {
|
|
673
|
+
id: 'deepseek',
|
|
674
|
+
name: 'DeepSeek',
|
|
675
|
+
baseUrl: 'https://api.deepseek.com',
|
|
676
|
+
auth: ['api-key'],
|
|
677
|
+
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
678
|
+
oauthProviderId: null,
|
|
679
|
+
models: {
|
|
680
|
+
'deepseek-v4-flash': {
|
|
681
|
+
id: 'deepseek-v4-flash',
|
|
682
|
+
name: 'DeepSeek V4 Flash',
|
|
683
|
+
api: 'openai-completions',
|
|
684
|
+
provider: 'deepseek',
|
|
685
|
+
baseUrl: 'https://api.deepseek.com',
|
|
686
|
+
reasoning: true,
|
|
687
|
+
input: ['text'],
|
|
688
|
+
cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
689
|
+
contextWindow: 1000000,
|
|
690
|
+
maxTokens: 384000,
|
|
691
|
+
},
|
|
692
|
+
'deepseek-v4-pro': {
|
|
693
|
+
id: 'deepseek-v4-pro',
|
|
694
|
+
name: 'DeepSeek V4 Pro',
|
|
695
|
+
api: 'openai-completions',
|
|
696
|
+
provider: 'deepseek',
|
|
697
|
+
baseUrl: 'https://api.deepseek.com',
|
|
698
|
+
reasoning: true,
|
|
699
|
+
input: ['text'],
|
|
700
|
+
cost: { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
|
701
|
+
contextWindow: 1000000,
|
|
702
|
+
maxTokens: 384000,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
},
|
|
456
706
|
} as const satisfies Record<string, KnownProvider>
|
|
457
707
|
|
|
458
708
|
export type KnownProviderId = keyof typeof KNOWN_PROVIDERS
|
|
459
709
|
|
|
710
|
+
// UX-only grouping of provider ids under one vendor for the init/`provider
|
|
711
|
+
// add` pickers. Deliberately does NOT touch the runtime contract:
|
|
712
|
+
// `KnownProviderId`, `KnownModelRef`, secrets.json keys, auth resolution, and
|
|
713
|
+
// the generated schema all stay keyed on the flat ids in `KNOWN_PROVIDERS`.
|
|
714
|
+
// The follow-up "variant" prompt resolves a concrete provider id, then
|
|
715
|
+
// `pickAuthMethod` runs as before; it is auto-resolved for single-provider
|
|
716
|
+
// vendors (Fireworks, Anthropic). `variants` copy lets the prompt read as an
|
|
717
|
+
// auth choice for OpenAI but a plan choice for Z.AI (both api-key, different
|
|
718
|
+
// billing surfaces).
|
|
719
|
+
type KnownProviderVendor = {
|
|
720
|
+
id: string
|
|
721
|
+
name: string
|
|
722
|
+
providers: ReadonlyArray<KnownProviderId>
|
|
723
|
+
variants?: Partial<Record<KnownProviderId, { label: string; hint?: string }>>
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Ordered by product priority for the picker — independent of the
|
|
727
|
+
// `KNOWN_PROVIDERS` declaration order (which stays load-bearing for the schema
|
|
728
|
+
// enum and `provider --help` listing). Every provider id below MUST appear in
|
|
729
|
+
// exactly one vendor; `providers.test.ts` enforces the partition.
|
|
730
|
+
export const KNOWN_PROVIDER_VENDORS = {
|
|
731
|
+
openai: {
|
|
732
|
+
id: 'openai',
|
|
733
|
+
name: 'OpenAI',
|
|
734
|
+
providers: ['openai', 'openai-codex'],
|
|
735
|
+
variants: {
|
|
736
|
+
openai: { label: 'API key', hint: 'OpenAI API platform' },
|
|
737
|
+
'openai-codex': { label: 'OAuth (ChatGPT Plus/Pro)', hint: 'ChatGPT subscription' },
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
anthropic: {
|
|
741
|
+
id: 'anthropic',
|
|
742
|
+
name: 'Anthropic',
|
|
743
|
+
providers: ['anthropic'],
|
|
744
|
+
},
|
|
745
|
+
fireworks: {
|
|
746
|
+
id: 'fireworks',
|
|
747
|
+
name: 'Fireworks',
|
|
748
|
+
providers: ['fireworks'],
|
|
749
|
+
},
|
|
750
|
+
zai: {
|
|
751
|
+
id: 'zai',
|
|
752
|
+
name: 'Z.AI',
|
|
753
|
+
providers: ['zai', 'zai-coding'],
|
|
754
|
+
variants: {
|
|
755
|
+
zai: { label: 'Pay-as-you-go', hint: 'standard API billing' },
|
|
756
|
+
'zai-coding': { label: 'Coding Plan', hint: 'GLM Coding Plan subscription' },
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
xai: {
|
|
760
|
+
id: 'xai',
|
|
761
|
+
name: 'xAI (Grok)',
|
|
762
|
+
providers: ['xai'],
|
|
763
|
+
},
|
|
764
|
+
// Single-provider vendor: pay-as-you-go and Token Plan are the SAME provider
|
|
765
|
+
// id (same /v1 endpoint, same Bearer transport — only the key prefix differs),
|
|
766
|
+
// so per the granularity rule above MiniMax is one id like Anthropic, not a
|
|
767
|
+
// zai-style split. The picker shows one row and skips the variant prompt; the
|
|
768
|
+
// pay-as-you-go-vs-Token-Plan dashboard hint is handled in the key-entry step.
|
|
769
|
+
minimax: {
|
|
770
|
+
id: 'minimax',
|
|
771
|
+
name: 'MiniMax',
|
|
772
|
+
providers: ['minimax'],
|
|
773
|
+
},
|
|
774
|
+
deepseek: {
|
|
775
|
+
id: 'deepseek',
|
|
776
|
+
name: 'DeepSeek',
|
|
777
|
+
providers: ['deepseek'],
|
|
778
|
+
},
|
|
779
|
+
} as const satisfies Record<string, KnownProviderVendor>
|
|
780
|
+
|
|
781
|
+
export type KnownProviderVendorId = keyof typeof KNOWN_PROVIDER_VENDORS
|
|
782
|
+
|
|
783
|
+
export function listKnownProviderVendorIds(): KnownProviderVendorId[] {
|
|
784
|
+
return Object.keys(KNOWN_PROVIDER_VENDORS) as KnownProviderVendorId[]
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function providerIdsForVendor(vendorId: KnownProviderVendorId): ReadonlyArray<KnownProviderId> {
|
|
788
|
+
return KNOWN_PROVIDER_VENDORS[vendorId].providers
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function vendorForProviderId(providerId: KnownProviderId): KnownProviderVendorId {
|
|
792
|
+
for (const vendorId of listKnownProviderVendorIds()) {
|
|
793
|
+
if ((KNOWN_PROVIDER_VENDORS[vendorId].providers as ReadonlyArray<KnownProviderId>).includes(providerId)) {
|
|
794
|
+
return vendorId
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
throw new Error(`Provider ${providerId} is not assigned to any vendor in KNOWN_PROVIDER_VENDORS`)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function variantCopy(
|
|
801
|
+
vendorId: KnownProviderVendorId,
|
|
802
|
+
providerId: KnownProviderId,
|
|
803
|
+
): { label: string; hint?: string } | undefined {
|
|
804
|
+
const vendor: KnownProviderVendor = KNOWN_PROVIDER_VENDORS[vendorId]
|
|
805
|
+
return vendor.variants?.[providerId]
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Falls back to the provider's own name when a vendor supplies no variant copy
|
|
809
|
+
// (single-provider vendors never render this prompt, so the fallback only
|
|
810
|
+
// guards against an incomplete `variants` map on a multi-provider vendor).
|
|
811
|
+
export function variantLabel(vendorId: KnownProviderVendorId, providerId: KnownProviderId): string {
|
|
812
|
+
return variantCopy(vendorId, providerId)?.label ?? KNOWN_PROVIDERS[providerId].name
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function variantHint(vendorId: KnownProviderVendorId, providerId: KnownProviderId): string | undefined {
|
|
816
|
+
return variantCopy(vendorId, providerId)?.hint
|
|
817
|
+
}
|
|
818
|
+
|
|
460
819
|
export type KnownModelRef = {
|
|
461
820
|
[P in KnownProviderId]: `${P}/${Extract<keyof (typeof KNOWN_PROVIDERS)[P]['models'], string>}`
|
|
462
821
|
}[KnownProviderId]
|
package/src/cron/consumer.ts
CHANGED
|
@@ -56,9 +56,22 @@ export type CreateCronConsumerOptions = {
|
|
|
56
56
|
// `ctx.exec`. Optional so unit-test fakes that never schedule handler jobs
|
|
57
57
|
// stay one-liners.
|
|
58
58
|
invokeHandler?: CronHandlerInvoker
|
|
59
|
+
// Authoritative count gate. The consumer — not the scheduler — owns
|
|
60
|
+
// accepted-fire accounting: it re-checks the durable count and increments
|
|
61
|
+
// only for runs that pass coalescing, so a coalesced skip never consumes a
|
|
62
|
+
// count. Optional so test fakes that don't exercise counts stay one-liners.
|
|
63
|
+
countStore?: ConsumerCountStore
|
|
64
|
+
now?: () => number
|
|
59
65
|
logger?: CronConsumerLogger
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
export type ConsumerCountStore = {
|
|
69
|
+
get: (id: string, job: CronJob) => number
|
|
70
|
+
// Resolves true if the fire was accepted/counted, false if the job is no
|
|
71
|
+
// longer live (so the consumer skips dispatching stale config).
|
|
72
|
+
increment: (id: string, job: CronJob, at: number) => Promise<boolean>
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
export type CronConsumer = {
|
|
63
76
|
start: () => void
|
|
64
77
|
stop: () => void
|
|
@@ -76,6 +89,8 @@ export function createCronConsumer({
|
|
|
76
89
|
cwd,
|
|
77
90
|
createSessionForCron,
|
|
78
91
|
invokeHandler,
|
|
92
|
+
countStore,
|
|
93
|
+
now = Date.now,
|
|
79
94
|
logger = consoleLogger,
|
|
80
95
|
}: CreateCronConsumerOptions): CronConsumer {
|
|
81
96
|
const inFlight = new Set<string>()
|
|
@@ -94,8 +109,26 @@ export function createCronConsumer({
|
|
|
94
109
|
logger.warn(`[cron] ${job.id}: previous run still in progress, skipping`)
|
|
95
110
|
return
|
|
96
111
|
}
|
|
112
|
+
// Reserve before the count gate so two close occurrences can't both
|
|
113
|
+
// pass the `firedCount < count` check before either increment lands.
|
|
97
114
|
inFlight.add(job.id)
|
|
98
115
|
try {
|
|
116
|
+
if (job.count !== undefined && countStore !== undefined) {
|
|
117
|
+
if (countStore.get(job.id, job) >= job.count) {
|
|
118
|
+
logger.info(`[cron] ${job.id}: count boundary reached, skipping`)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
// Durably record the accepted fire BEFORE dispatch. A crash here
|
|
122
|
+
// consumes the count without running (at-most-count), which is the
|
|
123
|
+
// correct tradeoff for a reminder versus over-firing on restart.
|
|
124
|
+
// A false result means a reload removed/replaced the job while the
|
|
125
|
+
// write was queued — skip dispatch so we never run stale config.
|
|
126
|
+
const accepted = await countStore.increment(job.id, job, now())
|
|
127
|
+
if (!accepted) {
|
|
128
|
+
logger.info(`[cron] ${job.id}: job no longer live, skipping dispatch`)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
}
|
|
99
132
|
if (job.kind === 'prompt') {
|
|
100
133
|
await runPrompt(job, createSessionForCron, stream, logger)
|
|
101
134
|
} else if (job.kind === 'exec') {
|