infinicode 1.0.0 → 2.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.
Files changed (157) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +120 -0
  2. package/.opencode/plugins/routing-mode-display.tsx +135 -0
  3. package/.opencode/themes/infinibot-gold.json +222 -0
  4. package/.opencode/tui.json +8 -0
  5. package/README.md +527 -75
  6. package/dist/ascii-video-animation.d.ts +2 -0
  7. package/dist/ascii-video-animation.js +243 -0
  8. package/dist/cli.js +195 -50
  9. package/dist/commands/console.d.ts +6 -0
  10. package/dist/commands/console.js +111 -0
  11. package/dist/commands/kernel-setup.d.ts +3 -0
  12. package/dist/commands/kernel-setup.js +303 -0
  13. package/dist/commands/mcp.d.ts +12 -0
  14. package/dist/commands/mcp.js +96 -0
  15. package/dist/commands/mission.d.ts +16 -0
  16. package/dist/commands/mission.js +301 -0
  17. package/dist/commands/models.d.ts +3 -1
  18. package/dist/commands/models.js +111 -55
  19. package/dist/commands/providers.d.ts +6 -0
  20. package/dist/commands/providers.js +95 -0
  21. package/dist/commands/run.d.ts +2 -1
  22. package/dist/commands/run.js +349 -59
  23. package/dist/commands/serve.d.ts +18 -0
  24. package/dist/commands/serve.js +127 -0
  25. package/dist/commands/setup.d.ts +1 -1
  26. package/dist/commands/setup.js +77 -44
  27. package/dist/commands/status.d.ts +2 -1
  28. package/dist/commands/status.js +46 -30
  29. package/dist/commands/workers.d.ts +5 -0
  30. package/dist/commands/workers.js +103 -0
  31. package/dist/kernel/autonomy/goal-loop.d.ts +49 -0
  32. package/dist/kernel/autonomy/goal-loop.js +113 -0
  33. package/dist/kernel/autonomy/index.d.ts +8 -0
  34. package/dist/kernel/autonomy/index.js +7 -0
  35. package/dist/kernel/browser/browser-controller.d.ts +57 -0
  36. package/dist/kernel/browser/browser-controller.js +175 -0
  37. package/dist/kernel/checkpoint-engine.d.ts +17 -0
  38. package/dist/kernel/checkpoint-engine.js +128 -0
  39. package/dist/kernel/command-system.d.ts +37 -0
  40. package/dist/kernel/command-system.js +239 -0
  41. package/dist/kernel/config-schema.d.ts +53 -0
  42. package/dist/kernel/config-schema.js +36 -0
  43. package/dist/kernel/event-bus.d.ts +19 -0
  44. package/dist/kernel/event-bus.js +65 -0
  45. package/dist/kernel/federation/auto-update.d.ts +36 -0
  46. package/dist/kernel/federation/auto-update.js +57 -0
  47. package/dist/kernel/federation/compute-router.d.ts +27 -0
  48. package/dist/kernel/federation/compute-router.js +44 -0
  49. package/dist/kernel/federation/config-sync.d.ts +33 -0
  50. package/dist/kernel/federation/config-sync.js +37 -0
  51. package/dist/kernel/federation/discovery.d.ts +11 -0
  52. package/dist/kernel/federation/discovery.js +44 -0
  53. package/dist/kernel/federation/event-bridge.d.ts +30 -0
  54. package/dist/kernel/federation/event-bridge.js +61 -0
  55. package/dist/kernel/federation/federation-plugin.d.ts +24 -0
  56. package/dist/kernel/federation/federation-plugin.js +35 -0
  57. package/dist/kernel/federation/federation.d.ts +126 -0
  58. package/dist/kernel/federation/federation.js +335 -0
  59. package/dist/kernel/federation/index.d.ts +31 -0
  60. package/dist/kernel/federation/index.js +23 -0
  61. package/dist/kernel/federation/moltfed-adapter.d.ts +30 -0
  62. package/dist/kernel/federation/moltfed-adapter.js +52 -0
  63. package/dist/kernel/federation/moltfed-connector.d.ts +98 -0
  64. package/dist/kernel/federation/moltfed-connector.js +193 -0
  65. package/dist/kernel/federation/node-identity.d.ts +19 -0
  66. package/dist/kernel/federation/node-identity.js +86 -0
  67. package/dist/kernel/federation/peer-mesh.d.ts +46 -0
  68. package/dist/kernel/federation/peer-mesh.js +117 -0
  69. package/dist/kernel/federation/protocol.d.ts +20 -0
  70. package/dist/kernel/federation/protocol.js +61 -0
  71. package/dist/kernel/federation/role-context.d.ts +5 -0
  72. package/dist/kernel/federation/role-context.js +45 -0
  73. package/dist/kernel/federation/telemetry.d.ts +21 -0
  74. package/dist/kernel/federation/telemetry.js +138 -0
  75. package/dist/kernel/federation/transport-http.d.ts +44 -0
  76. package/dist/kernel/federation/transport-http.js +207 -0
  77. package/dist/kernel/federation/types.d.ts +212 -0
  78. package/dist/kernel/federation/types.js +3 -0
  79. package/dist/kernel/free-providers.d.ts +28 -0
  80. package/dist/kernel/free-providers.js +162 -0
  81. package/dist/kernel/frontend-scoring.d.ts +21 -0
  82. package/dist/kernel/frontend-scoring.js +125 -0
  83. package/dist/kernel/index.d.ts +63 -0
  84. package/dist/kernel/index.js +45 -0
  85. package/dist/kernel/kernel.d.ts +75 -0
  86. package/dist/kernel/kernel.js +223 -0
  87. package/dist/kernel/logger.d.ts +18 -0
  88. package/dist/kernel/logger.js +26 -0
  89. package/dist/kernel/mcp/index.d.ts +9 -0
  90. package/dist/kernel/mcp/index.js +8 -0
  91. package/dist/kernel/mcp/mcp-server.d.ts +27 -0
  92. package/dist/kernel/mcp/mcp-server.js +178 -0
  93. package/dist/kernel/mission-engine.d.ts +42 -0
  94. package/dist/kernel/mission-engine.js +160 -0
  95. package/dist/kernel/orchestrator.d.ts +51 -0
  96. package/dist/kernel/orchestrator.js +226 -0
  97. package/dist/kernel/plugin-manager.d.ts +28 -0
  98. package/dist/kernel/plugin-manager.js +76 -0
  99. package/dist/kernel/plugins/browser-plugin.d.ts +22 -0
  100. package/dist/kernel/plugins/browser-plugin.js +34 -0
  101. package/dist/kernel/plugins/dashboard-plugin.d.ts +17 -0
  102. package/dist/kernel/plugins/dashboard-plugin.js +72 -0
  103. package/dist/kernel/plugins/discord-plugin.d.ts +11 -0
  104. package/dist/kernel/plugins/discord-plugin.js +35 -0
  105. package/dist/kernel/plugins/metrics-plugin.d.ts +33 -0
  106. package/dist/kernel/plugins/metrics-plugin.js +86 -0
  107. package/dist/kernel/plugins/search-plugin.d.ts +17 -0
  108. package/dist/kernel/plugins/search-plugin.js +20 -0
  109. package/dist/kernel/plugins/slack-plugin.d.ts +11 -0
  110. package/dist/kernel/plugins/slack-plugin.js +35 -0
  111. package/dist/kernel/plugins/telegram-plugin.d.ts +20 -0
  112. package/dist/kernel/plugins/telegram-plugin.js +122 -0
  113. package/dist/kernel/policies.d.ts +33 -0
  114. package/dist/kernel/policies.js +105 -0
  115. package/dist/kernel/provider-discovery.d.ts +41 -0
  116. package/dist/kernel/provider-discovery.js +179 -0
  117. package/dist/kernel/provider-manager.d.ts +38 -0
  118. package/dist/kernel/provider-manager.js +206 -0
  119. package/dist/kernel/provider-url.d.ts +18 -0
  120. package/dist/kernel/provider-url.js +45 -0
  121. package/dist/kernel/providers/gemini-provider.d.ts +43 -0
  122. package/dist/kernel/providers/gemini-provider.js +203 -0
  123. package/dist/kernel/providers/ollama-provider.d.ts +44 -0
  124. package/dist/kernel/providers/ollama-provider.js +241 -0
  125. package/dist/kernel/providers/openai-compatible-provider.d.ts +43 -0
  126. package/dist/kernel/providers/openai-compatible-provider.js +233 -0
  127. package/dist/kernel/recovery-manager.d.ts +38 -0
  128. package/dist/kernel/recovery-manager.js +156 -0
  129. package/dist/kernel/router.d.ts +44 -0
  130. package/dist/kernel/router.js +222 -0
  131. package/dist/kernel/sample-missions.d.ts +11 -0
  132. package/dist/kernel/sample-missions.js +132 -0
  133. package/dist/kernel/scheduler.d.ts +30 -0
  134. package/dist/kernel/scheduler.js +147 -0
  135. package/dist/kernel/sdk/harness-sdk.d.ts +37 -0
  136. package/dist/kernel/sdk/harness-sdk.js +48 -0
  137. package/dist/kernel/sdk/plugin-sdk.d.ts +27 -0
  138. package/dist/kernel/sdk/plugin-sdk.js +40 -0
  139. package/dist/kernel/search/search-controller.d.ts +32 -0
  140. package/dist/kernel/search/search-controller.js +141 -0
  141. package/dist/kernel/setup.d.ts +13 -0
  142. package/dist/kernel/setup.js +59 -0
  143. package/dist/kernel/types.d.ts +479 -0
  144. package/dist/kernel/types.js +7 -0
  145. package/dist/kernel/verification-engine.d.ts +33 -0
  146. package/dist/kernel/verification-engine.js +287 -0
  147. package/dist/kernel/worker-runtime.d.ts +66 -0
  148. package/dist/kernel/worker-runtime.js +261 -0
  149. package/dist/kernel/workers/browser-worker.d.ts +6 -0
  150. package/dist/kernel/workers/browser-worker.js +92 -0
  151. package/dist/kernel/workers/builtin-workers.d.ts +20 -0
  152. package/dist/kernel/workers/builtin-workers.js +120 -0
  153. package/dist/kernel/workers/research-worker.d.ts +5 -0
  154. package/dist/kernel/workers/research-worker.js +90 -0
  155. package/dist/prompt-ascii-video-animation.d.ts +2 -0
  156. package/dist/prompt-ascii-video-animation.js +243 -0
  157. package/package.json +43 -9
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Provider connection testing + model discovery.
3
+ *
4
+ * Used by the setup wizard to verify credentials and enumerate models
5
+ * across Ollama + all configured cloud providers.
6
+ */
7
+ import ora from 'ora';
8
+ import chalk from 'chalk';
9
+ import { OllamaProvider, OpenAICompatibleProvider, GeminiProvider } from '../kernel/index.js';
10
+ import { getProviderPreset, FREE_PROVIDERS } from './free-providers.js';
11
+ export async function testOllamaProvider(baseURL, fallbackURL, timeoutMs = 8000) {
12
+ const provider = new OllamaProvider({ baseURL, fallbackURL, timeoutMs });
13
+ const start = Date.now();
14
+ try {
15
+ const caps = await provider.capabilities();
16
+ return {
17
+ providerId: 'ollama',
18
+ name: 'Ollama',
19
+ ok: true,
20
+ modelCount: caps.models.length,
21
+ latencyMs: Date.now() - start,
22
+ };
23
+ }
24
+ catch (err) {
25
+ return {
26
+ providerId: 'ollama',
27
+ name: 'Ollama',
28
+ ok: false,
29
+ error: err instanceof Error ? err.message : String(err),
30
+ modelCount: 0,
31
+ };
32
+ }
33
+ }
34
+ export async function testCloudProvider(cfg, timeoutMs = 8000) {
35
+ const preset = getProviderPreset(cfg.id);
36
+ const start = Date.now();
37
+ try {
38
+ const caps = cfg.id === 'gemini'
39
+ ? await new GeminiProvider({
40
+ id: cfg.id,
41
+ name: cfg.name,
42
+ apiKey: cfg.apiKey ?? '',
43
+ baseURL: cfg.baseURL,
44
+ timeoutMs,
45
+ knownModels: preset?.knownModels ?? [],
46
+ }).capabilities()
47
+ : await new OpenAICompatibleProvider({
48
+ id: cfg.id,
49
+ name: cfg.name,
50
+ baseURL: cfg.baseURL,
51
+ apiKey: cfg.apiKey,
52
+ headerName: cfg.headerName,
53
+ timeoutMs,
54
+ knownModels: preset?.knownModels ?? [],
55
+ }).capabilities();
56
+ return {
57
+ providerId: cfg.id,
58
+ name: cfg.name,
59
+ ok: true,
60
+ modelCount: caps.models.length,
61
+ latencyMs: Date.now() - start,
62
+ };
63
+ }
64
+ catch (err) {
65
+ return {
66
+ providerId: cfg.id,
67
+ name: cfg.name,
68
+ ok: false,
69
+ error: err instanceof Error ? err.message : String(err),
70
+ modelCount: 0,
71
+ };
72
+ }
73
+ }
74
+ export async function testProviderInteractive(label, fn) {
75
+ const spinner = ora(`Testing ${label}...`).start();
76
+ const result = await fn();
77
+ if (result.ok) {
78
+ spinner.succeed(`${label} connected — ${chalk.green(String(result.modelCount))} models, ${result.latencyMs ?? 0}ms`);
79
+ }
80
+ else {
81
+ spinner.fail(`${label} failed: ${result.error ?? 'unknown error'}`);
82
+ }
83
+ return result;
84
+ }
85
+ /**
86
+ * Discover all available models across all configured providers (Ollama + cloud).
87
+ * Returns a flat list tagged with providerId.
88
+ */
89
+ export async function discoverAllModels(opts) {
90
+ const all = [];
91
+ // Ollama
92
+ try {
93
+ const provider = new OllamaProvider({ baseURL: opts.masterUrl, fallbackURL: opts.tailscaleMasterUrl });
94
+ const caps = await provider.capabilities();
95
+ for (const model of caps.models) {
96
+ all.push({ providerId: 'ollama', model });
97
+ }
98
+ }
99
+ catch {
100
+ // ollama offline — continue with cloud
101
+ }
102
+ // Cloud providers
103
+ for (const cfg of opts.cloudProviders) {
104
+ if (!cfg.enabled)
105
+ continue;
106
+ const preset = getProviderPreset(cfg.id);
107
+ try {
108
+ const provider = cfg.id === 'gemini'
109
+ ? new GeminiProvider({
110
+ id: cfg.id,
111
+ name: cfg.name,
112
+ apiKey: cfg.apiKey ?? '',
113
+ baseURL: cfg.baseURL,
114
+ knownModels: preset?.knownModels ?? [],
115
+ })
116
+ : new OpenAICompatibleProvider({
117
+ id: cfg.id,
118
+ name: cfg.name,
119
+ baseURL: cfg.baseURL,
120
+ apiKey: cfg.apiKey,
121
+ headerName: cfg.headerName,
122
+ knownModels: preset?.knownModels ?? [],
123
+ });
124
+ const caps = await provider.capabilities();
125
+ for (const model of caps.models) {
126
+ all.push({ providerId: cfg.id, model });
127
+ }
128
+ }
129
+ catch {
130
+ // provider offline — continue
131
+ }
132
+ }
133
+ return all;
134
+ }
135
+ /**
136
+ * Build a friendly display label for a discovered model.
137
+ */
138
+ export function modelLabel(dm) {
139
+ const ctx = dm.model.contextLength >= 1000
140
+ ? `${Math.round(dm.model.contextLength / 1000)}k`
141
+ : `${dm.model.contextLength}`;
142
+ const caps = dm.model.capabilities.slice(0, 3).join(',');
143
+ return `${dm.providerId}/${dm.model.id} ${chalk.dim(`ctx=${ctx} caps=${caps}`)}`;
144
+ }
145
+ /**
146
+ * Suggest a recommended model for a worker type from the discovered pool.
147
+ * Falls back to the provider preset's recommended list if discovery found nothing.
148
+ */
149
+ export function suggestModelForWorker(workerType, discovered) {
150
+ // 1. try preset recommendations that match discovered models
151
+ for (const preset of FREE_PROVIDERS) {
152
+ const recs = preset.recommended[workerType] ?? [];
153
+ for (const recId of recs) {
154
+ const hit = discovered.find(dm => dm.providerId === preset.id && dm.model.id === recId);
155
+ if (hit)
156
+ return hit;
157
+ }
158
+ }
159
+ // 2. any discovered model that has the matching capability
160
+ const capability = workerType === 'coding' ? 'coding'
161
+ : workerType === 'vision' ? 'vision'
162
+ : workerType === 'review' ? 'review'
163
+ : 'reasoning';
164
+ return discovered.find(dm => dm.model.capabilities.includes(capability)) ?? discovered[0];
165
+ }
166
+ /**
167
+ * Build a complete workerModels map by suggesting a model for each builtin worker type.
168
+ */
169
+ export function suggestAllWorkerModels(discovered) {
170
+ const out = {};
171
+ const types = ['coding', 'research', 'architecture', 'review', 'documentation', 'translation', 'terminal', 'verification', 'vision', 'browser'];
172
+ for (const t of types) {
173
+ const hit = suggestModelForWorker(t, discovered);
174
+ if (hit) {
175
+ out[t] = { providerId: hit.providerId, modelId: hit.model.id };
176
+ }
177
+ }
178
+ return out;
179
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * OpenKernel — Provider Manager
3
+ *
4
+ * Tracks available models, quota, latency, health, capabilities.
5
+ * Tracks 429s, quota remaining, per-request success rate.
6
+ * Background refresh only. Never blocks execution.
7
+ */
8
+ import type { ProviderInfo, ProviderInterface, ProviderModel } from './types.js';
9
+ import type { EventBus } from './event-bus.js';
10
+ import type { Logger } from './types.js';
11
+ export declare class ProviderManager {
12
+ private eventBus;
13
+ private logger;
14
+ private providers;
15
+ private healthTimers;
16
+ private refreshIntervalMs;
17
+ private stats;
18
+ constructor(eventBus: EventBus, logger: Logger);
19
+ register(providerId: string, provider: ProviderInterface): void;
20
+ unregister(providerId: string): void;
21
+ get(providerId: string): ProviderInterface | undefined;
22
+ list(): ProviderInterface[];
23
+ listInfos(): ProviderInfo[];
24
+ getAvailableModels(): Promise<ProviderModel[]>;
25
+ getHealthyProviders(): Promise<ProviderInterface[]>;
26
+ /** Record a successful completion for success-rate + quota tracking. */
27
+ recordSuccess(providerId: string): void;
28
+ /** Record a failed completion (non-429) for success-rate tracking. */
29
+ recordFailure(providerId: string): void;
30
+ /** Record a 429 / rate-limit hit — decrements quota and may mark unhealthy. */
31
+ record429(providerId: string): void;
32
+ private pushQuota;
33
+ private estimateQuota;
34
+ private scheduleHealthCheck;
35
+ checkHealth(providerId: string): Promise<void>;
36
+ setRefreshInterval(ms: number): void;
37
+ destroy(): void;
38
+ }
@@ -0,0 +1,206 @@
1
+ const QUOTA_WINDOW_MS = 60_000;
2
+ const QUOTA_MAX_PER_MIN = 60;
3
+ // A real 1-token completion costs a little, so re-verify with a token at most
4
+ // this often; cheaper /models refreshes fill the gaps between verifications.
5
+ const VERIFY_INTERVAL_MS = 5 * 60_000;
6
+ export class ProviderManager {
7
+ eventBus;
8
+ logger;
9
+ providers = new Map();
10
+ healthTimers = new Map();
11
+ refreshIntervalMs = 60_000;
12
+ stats = new Map();
13
+ constructor(eventBus, logger) {
14
+ this.eventBus = eventBus;
15
+ this.logger = logger;
16
+ }
17
+ register(providerId, provider) {
18
+ this.providers.set(providerId, provider);
19
+ this.stats.set(providerId, {
20
+ totalRequests: 0,
21
+ successfulRequests: 0,
22
+ failedRequests: 0,
23
+ consecutiveFailures: 0,
24
+ quotaWindow: [],
25
+ });
26
+ this.logger.info(`Provider registered: ${providerId}`);
27
+ this.scheduleHealthCheck(providerId);
28
+ void this.checkHealth(providerId);
29
+ }
30
+ unregister(providerId) {
31
+ this.providers.delete(providerId);
32
+ this.stats.delete(providerId);
33
+ const timer = this.healthTimers.get(providerId);
34
+ if (timer) {
35
+ clearInterval(timer);
36
+ this.healthTimers.delete(providerId);
37
+ }
38
+ }
39
+ get(providerId) {
40
+ return this.providers.get(providerId);
41
+ }
42
+ list() {
43
+ return [...this.providers.values()];
44
+ }
45
+ listInfos() {
46
+ return this.list().map(p => p.info);
47
+ }
48
+ async getAvailableModels() {
49
+ const models = [];
50
+ for (const provider of this.providers.values()) {
51
+ if (!provider.info.healthy)
52
+ continue;
53
+ try {
54
+ const caps = await provider.capabilities();
55
+ models.push(...caps.models);
56
+ }
57
+ catch (err) {
58
+ this.logger.warn(`Failed to get models from ${provider.info.id}: ${err}`);
59
+ }
60
+ }
61
+ return models;
62
+ }
63
+ async getHealthyProviders() {
64
+ return this.list().filter(p => p.info.healthy);
65
+ }
66
+ /** Record a successful completion for success-rate + quota tracking. */
67
+ recordSuccess(providerId) {
68
+ const s = this.stats.get(providerId);
69
+ const info = this.providers.get(providerId)?.info;
70
+ if (s) {
71
+ s.totalRequests++;
72
+ s.successfulRequests++;
73
+ s.consecutiveFailures = 0;
74
+ this.pushQuota(s);
75
+ if (info) {
76
+ info.successRate = s.totalRequests > 0 ? s.successfulRequests / s.totalRequests : undefined;
77
+ info.quotaRemaining = this.estimateQuota(s);
78
+ }
79
+ }
80
+ }
81
+ /** Record a failed completion (non-429) for success-rate tracking. */
82
+ recordFailure(providerId) {
83
+ const s = this.stats.get(providerId);
84
+ const info = this.providers.get(providerId)?.info;
85
+ if (s) {
86
+ s.totalRequests++;
87
+ s.failedRequests++;
88
+ s.consecutiveFailures++;
89
+ if (info) {
90
+ info.successRate = s.totalRequests > 0 ? s.successfulRequests / s.totalRequests : undefined;
91
+ if (s.consecutiveFailures >= 3) {
92
+ info.healthy = false;
93
+ info.error = `${s.consecutiveFailures} consecutive failures`;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ /** Record a 429 / rate-limit hit — decrements quota and may mark unhealthy. */
99
+ record429(providerId) {
100
+ const s = this.stats.get(providerId);
101
+ const info = this.providers.get(providerId)?.info;
102
+ if (s) {
103
+ s.totalRequests++;
104
+ s.failedRequests++;
105
+ s.consecutiveFailures++;
106
+ s.last429At = Date.now();
107
+ if (info) {
108
+ info.quotaRemaining = Math.max(0, (info.quotaRemaining ?? 0) - 1);
109
+ info.successRate = s.totalRequests > 0 ? s.successfulRequests / s.totalRequests : undefined;
110
+ if ((info.quotaRemaining ?? 0) <= 0) {
111
+ info.healthy = false;
112
+ info.error = 'rate-limited (quota exhausted)';
113
+ }
114
+ }
115
+ }
116
+ }
117
+ pushQuota(s) {
118
+ const now = Date.now();
119
+ s.quotaWindow.push(now);
120
+ s.quotaWindow = s.quotaWindow.filter(t => now - t < QUOTA_WINDOW_MS);
121
+ }
122
+ estimateQuota(s) {
123
+ const now = Date.now();
124
+ const recent = s.quotaWindow.filter(t => now - t < QUOTA_WINDOW_MS).length;
125
+ return Math.max(0, QUOTA_MAX_PER_MIN - recent);
126
+ }
127
+ scheduleHealthCheck(providerId) {
128
+ const timer = setInterval(() => {
129
+ void this.checkHealth(providerId);
130
+ }, this.refreshIntervalMs);
131
+ this.healthTimers.set(providerId, timer);
132
+ }
133
+ async checkHealth(providerId) {
134
+ const provider = this.providers.get(providerId);
135
+ if (!provider)
136
+ return;
137
+ const start = Date.now();
138
+ const s = this.stats.get(providerId);
139
+ const now = Date.now();
140
+ // Verify with a real token-authenticated completion on first check and
141
+ // periodically thereafter; use the cheap /models refresh in between.
142
+ const shouldVerify = typeof provider.verify === 'function' &&
143
+ (s?.lastVerifyAt === undefined || now - s.lastVerifyAt >= VERIFY_INTERVAL_MS);
144
+ try {
145
+ let latency;
146
+ if (shouldVerify && provider.verify) {
147
+ const result = await provider.verify();
148
+ if (s)
149
+ s.lastVerifyAt = Date.now();
150
+ if (!result.ok)
151
+ throw new Error(result.error ?? 'token verification failed');
152
+ latency = result.latencyMs;
153
+ // Real per-minute token limit from the tier's headers — applies to all
154
+ // of this provider's models in the router.
155
+ if (result.rateLimitTokens && result.rateLimitTokens > 0) {
156
+ provider.info.maxRequestTokens = result.rateLimitTokens;
157
+ }
158
+ }
159
+ else if (provider.refresh) {
160
+ await provider.refresh();
161
+ latency = Date.now() - start;
162
+ }
163
+ else {
164
+ await provider.capabilities();
165
+ latency = Date.now() - start;
166
+ }
167
+ provider.info.healthy = true;
168
+ provider.info.available = true;
169
+ provider.info.latencyMs = latency;
170
+ provider.info.lastCheckedAt = Date.now();
171
+ provider.info.error = undefined;
172
+ if (s)
173
+ s.consecutiveFailures = 0;
174
+ }
175
+ catch (err) {
176
+ provider.info.healthy = false;
177
+ provider.info.available = false;
178
+ provider.info.error = err instanceof Error ? err.message : String(err);
179
+ provider.info.lastCheckedAt = Date.now();
180
+ this.logger.warn(`Provider ${providerId} health check failed: ${provider.info.error}`);
181
+ }
182
+ const event = {
183
+ type: 'PROVIDER_HEALTH',
184
+ timestamp: Date.now(),
185
+ data: {
186
+ providerId,
187
+ healthy: provider.info.healthy,
188
+ latencyMs: provider.info.latencyMs,
189
+ successRate: provider.info.successRate,
190
+ quotaRemaining: provider.info.quotaRemaining,
191
+ },
192
+ };
193
+ this.eventBus.emit(event);
194
+ }
195
+ setRefreshInterval(ms) {
196
+ this.refreshIntervalMs = ms;
197
+ }
198
+ destroy() {
199
+ for (const timer of this.healthTimers.values()) {
200
+ clearInterval(timer);
201
+ }
202
+ this.healthTimers.clear();
203
+ this.providers.clear();
204
+ this.stats.clear();
205
+ }
206
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * OpenKernel — OpenAI-compatible base URL normalization.
3
+ *
4
+ * Different consumers append different suffixes to a provider base URL:
5
+ * - the kernel provider and the Vercel AI SDK both ultimately POST to
6
+ * `<base>/chat/completions` and GET `<base>/models`.
7
+ * Stored/preset base URLs are inconsistent — some include the API version
8
+ * segment (`/v1`), some don't, and a few providers speak `/chat/completions`
9
+ * directly with no version segment at all.
10
+ *
11
+ * `openAICompatBaseURL` returns the canonical base to which `/chat/completions`
12
+ * (or `/models`) should be appended — with exactly one version segment where
13
+ * the provider expects it. Using it on both surfaces guarantees the same,
14
+ * correct URL regardless of how the base was stored.
15
+ */
16
+ export declare function openAICompatBaseURL(baseURL: string): string;
17
+ export declare function openAICompatChatURL(baseURL: string): string;
18
+ export declare function openAICompatModelsURL(baseURL: string): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * OpenKernel — OpenAI-compatible base URL normalization.
3
+ *
4
+ * Different consumers append different suffixes to a provider base URL:
5
+ * - the kernel provider and the Vercel AI SDK both ultimately POST to
6
+ * `<base>/chat/completions` and GET `<base>/models`.
7
+ * Stored/preset base URLs are inconsistent — some include the API version
8
+ * segment (`/v1`), some don't, and a few providers speak `/chat/completions`
9
+ * directly with no version segment at all.
10
+ *
11
+ * `openAICompatBaseURL` returns the canonical base to which `/chat/completions`
12
+ * (or `/models`) should be appended — with exactly one version segment where
13
+ * the provider expects it. Using it on both surfaces guarantees the same,
14
+ * correct URL regardless of how the base was stored.
15
+ */
16
+ // Hosts whose OpenAI-compatible route lives directly under the host, with no
17
+ // `/v1` version segment (e.g. GitHub Models / Azure AI Inference).
18
+ const NO_VERSION_HOSTS = [
19
+ /(^|\.)models\.inference\.ai\.azure\.com$/i,
20
+ /(^|\.)models\.github\.ai$/i,
21
+ ];
22
+ export function openAICompatBaseURL(baseURL) {
23
+ const url = (baseURL ?? '').trim().replace(/\/+$/, '');
24
+ if (!url)
25
+ return url;
26
+ let host = '';
27
+ try {
28
+ host = new URL(url).host;
29
+ }
30
+ catch {
31
+ // Not a parseable URL — fall through to suffix checks below.
32
+ }
33
+ if (host && NO_VERSION_HOSTS.some(rx => rx.test(host)))
34
+ return url;
35
+ // Already carries an explicit version segment (…/v1, …/v1beta, …/v2).
36
+ if (/\/v\d+[a-z]*$/i.test(url))
37
+ return url;
38
+ return `${url}/v1`;
39
+ }
40
+ export function openAICompatChatURL(baseURL) {
41
+ return `${openAICompatBaseURL(baseURL)}/chat/completions`;
42
+ }
43
+ export function openAICompatModelsURL(baseURL) {
44
+ return `${openAICompatBaseURL(baseURL)}/models`;
45
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * OpenKernel — Gemini Provider
3
+ *
4
+ * Google Generative Language API (Gemini family).
5
+ * Implements the kernel's ProviderInterface against
6
+ * https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
7
+ * ...:streamGenerateContent
8
+ *
9
+ * Distinct from OpenAICompatibleProvider because Gemini's request/response
10
+ * shapes (contents/parts, safety ratings, candidate counts) differ materially.
11
+ */
12
+ import type { CompletionChunk, CompletionRequest, CompletionResult, ProviderCapabilities, ProviderInfo, ProviderInterface, ProviderModel } from '../types.js';
13
+ export interface GeminiProviderOptions {
14
+ id?: string;
15
+ name?: string;
16
+ apiKey: string;
17
+ /** Base URL without trailing slash. */
18
+ baseURL?: string;
19
+ timeoutMs?: number;
20
+ knownModels?: Array<Partial<ProviderModel> & {
21
+ id: string;
22
+ }>;
23
+ }
24
+ export declare class GeminiProvider implements ProviderInterface {
25
+ info: ProviderInfo;
26
+ private apiKey;
27
+ private baseURL;
28
+ private timeoutMs;
29
+ private knownModels;
30
+ constructor(options: GeminiProviderOptions);
31
+ capabilities(): Promise<ProviderCapabilities>;
32
+ complete(request: CompletionRequest): Promise<CompletionResult>;
33
+ completeStream(request: CompletionRequest): AsyncIterable<CompletionChunk>;
34
+ refresh(): Promise<void>;
35
+ verify(): Promise<{
36
+ ok: boolean;
37
+ latencyMs: number;
38
+ error?: string;
39
+ }>;
40
+ private buildBody;
41
+ private fetchModels;
42
+ private toProviderModel;
43
+ }