@ai-sdk/harness-pi 1.0.92 → 1.0.94

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-pi",
3
- "version": "1.0.92",
3
+ "version": "1.0.94",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@ai-sdk/harness": "1.0.90",
29
+ "@ai-sdk/harness": "1.0.92",
30
30
  "@ai-sdk/provider-utils": "5.0.32",
31
31
  "@earendil-works/pi-ai": "0.74.2",
32
32
  "@earendil-works/pi-coding-agent": "^0.80.10",
@@ -37,7 +37,7 @@
37
37
  "zod": "^3.25.76 || ^4.1.8"
38
38
  },
39
39
  "devDependencies": {
40
- "@ai-sdk/sandbox-just-bash": "1.0.90",
40
+ "@ai-sdk/sandbox-just-bash": "1.0.92",
41
41
  "@types/node": "22.19.19",
42
42
  "@vercel/ai-tsconfig": "0.0.0",
43
43
  "tsup": "^8.5.1",
package/src/index.ts CHANGED
@@ -9,4 +9,4 @@ export const pi = createPi();
9
9
  export { createPi } from './pi-harness';
10
10
  export { VERSION } from './version';
11
11
  export type { PiHarnessSettings } from './pi-harness';
12
- export type { PiAuthenticationMode, PiAuthOptions } from './pi-auth';
12
+ export type { PiAuthenticationMode } from './pi-auth';
package/src/pi-auth.ts CHANGED
@@ -1,51 +1,175 @@
1
- import type {
2
- ModelRegistry,
1
+ import {
3
2
  ModelRuntime,
3
+ type CreateModelRuntimeOptions,
4
+ type ModelRegistry,
4
5
  } from '@earendil-works/pi-coding-agent';
5
- import { getAiGatewayAuthFromEnv } from '@ai-sdk/harness/utils';
6
+ import type { HarnessV1Authentication } from '@ai-sdk/harness';
7
+ import {
8
+ getAiGatewayAuthFromEnv,
9
+ isHarnessAuthenticationEnvironment,
10
+ } from '@ai-sdk/harness/utils';
11
+ import { access } from 'node:fs/promises';
6
12
  import { VERSION } from './version';
7
13
 
8
14
  type ProviderConfigInput = Parameters<ModelRegistry['registerProvider']>[1];
15
+ type PiCredentialStore = NonNullable<CreateModelRuntimeOptions['credentials']>;
16
+ type PiCredential = Exclude<
17
+ Awaited<ReturnType<PiCredentialStore['read']>>,
18
+ undefined
19
+ >;
20
+ type PiModelRuntimeInternals = {
21
+ models: {
22
+ authContext: {
23
+ env(name: string): Promise<string | undefined>;
24
+ fileExists(path: string): Promise<boolean>;
25
+ };
26
+ };
27
+ };
28
+ type PiMutableProvider = {
29
+ auth: ReturnType<ModelRuntime['getProviders']>[number]['auth'];
30
+ };
9
31
 
10
32
  /**
11
33
  * Pi auth options. Choose an explicit mode or rely on 'auto' (precedence:
12
34
  * explicit gateway, then OpenAI / Anthropic / custom environment variables).
13
35
  */
14
- export type PiAuthenticationMode =
15
- | 'auto'
16
- | 'openai'
17
- | 'anthropic'
18
- | 'custom'
19
- | 'ai-gateway';
20
-
21
- /**
22
- * @deprecated Passing an object to auth options is deprecated. Use a `PiAuthenticationMode` string value ("auto" | "openai" | "anthropic" | "custom" | "ai-gateway") instead, and pass credentials via environment variables.
23
- */
24
- export type LegacyPiAuthOptions = {
25
- readonly gateway?: {
26
- readonly apiKey?: string;
27
- readonly baseUrl?: string;
28
- };
29
- /**
30
- * Resolved environment-variable pairs of the form `<PREFIX>_API_KEY` and
31
- * (optionally) `<PREFIX>_BASE_URL`. Special-cased prefixes:
32
- * - `AI_GATEWAY` → registers `vercel-ai-gateway`
33
- * - `OPENAI` → registers `openai`
34
- * - `ANTHROPIC` → registers `anthropic` (`ANTHROPIC_AUTH_TOKEN` adds a
35
- * bearer auth header)
36
- * Any other `<PREFIX>_API_KEY` with a matching `<PREFIX>_BASE_URL` is
37
- * registered as the lowercased, dash-separated prefix.
38
- */
39
- readonly customEnv?: Record<string, string>;
40
- };
41
-
42
- export type PiAuthOptions = PiAuthenticationMode | LegacyPiAuthOptions;
36
+ export type PiAuthenticationMode = HarnessV1Authentication<
37
+ 'openai' | 'anthropic' | 'custom'
38
+ >;
43
39
 
44
40
  const DEFAULT_GATEWAY_BASE_URL = 'https://ai-gateway.vercel.sh';
45
41
  const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
46
42
  const DEFAULT_ANTHROPIC_BASE_URL = 'https://api.anthropic.com';
47
43
  const HARNESS_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
48
44
 
45
+ function createIsolatedPiCredentialStore(): {
46
+ credentials: PiCredentialStore;
47
+ finishInitialization(): void;
48
+ } {
49
+ let initializing = true;
50
+ const bootstrapEnvironment = new Proxy<Record<string, string>>(
51
+ {},
52
+ {
53
+ get: (_target, property) =>
54
+ typeof property === 'string'
55
+ ? 'harness-pi-authentication-bootstrap'
56
+ : undefined,
57
+ },
58
+ );
59
+ const bootstrapCredential = {
60
+ type: 'api_key',
61
+ key: 'harness-pi-authentication-bootstrap',
62
+ env: bootstrapEnvironment,
63
+ } satisfies PiCredential;
64
+ const credentials: PiCredentialStore = {
65
+ async read() {
66
+ return initializing ? bootstrapCredential : undefined;
67
+ },
68
+ async list() {
69
+ return [];
70
+ },
71
+ async modify(..._input: Parameters<PiCredentialStore['modify']>) {
72
+ return undefined;
73
+ },
74
+ async delete() {},
75
+ };
76
+
77
+ return {
78
+ credentials,
79
+ finishInitialization() {
80
+ initializing = false;
81
+ },
82
+ };
83
+ }
84
+
85
+ function scopePiProviderEnvironment({
86
+ modelRuntime,
87
+ authenticationEnvironment,
88
+ }: {
89
+ modelRuntime: ModelRuntime;
90
+ authenticationEnvironment: Record<string, string>;
91
+ }): void {
92
+ for (const provider of modelRuntime.getProviders()) {
93
+ const apiKeyAuthentication = provider.auth.apiKey;
94
+ if (!apiKeyAuthentication) continue;
95
+
96
+ (provider as unknown as PiMutableProvider).auth = {
97
+ ...provider.auth,
98
+ apiKey: {
99
+ ...apiKeyAuthentication,
100
+ resolve: async input => {
101
+ const result = await apiKeyAuthentication.resolve(input);
102
+ return result
103
+ ? {
104
+ ...result,
105
+ env: {
106
+ ...authenticationEnvironment,
107
+ ...result.env,
108
+ },
109
+ }
110
+ : undefined;
111
+ },
112
+ },
113
+ };
114
+ }
115
+ }
116
+
117
+ export async function createPiModelRuntime({
118
+ auth,
119
+ authPath,
120
+ modelsPath,
121
+ }: {
122
+ auth: PiAuthenticationMode | undefined;
123
+ authPath: string;
124
+ modelsPath: string;
125
+ }): Promise<ModelRuntime> {
126
+ if (!isHarnessAuthenticationEnvironment(auth)) {
127
+ return ModelRuntime.create({
128
+ authPath,
129
+ modelsPath,
130
+ allowModelNetwork: false,
131
+ });
132
+ }
133
+
134
+ const isolatedCredentials = createIsolatedPiCredentialStore();
135
+ const modelRuntime = await ModelRuntime.create({
136
+ credentials: isolatedCredentials.credentials,
137
+ modelsPath: null,
138
+ allowModelNetwork: false,
139
+ });
140
+
141
+ /*
142
+ * ModelRuntime creates its internal model collection with a process-backed
143
+ * authentication context and does not expose an authentication-context
144
+ * option. The bootstrap credential prevents construction-time provider
145
+ * checks from consulting that context. Once constructed, authentication is
146
+ * scoped to the supplied record and availability is recomputed with an
147
+ * empty in-memory credential store.
148
+ */
149
+ (modelRuntime as unknown as PiModelRuntimeInternals).models.authContext = {
150
+ async env(name) {
151
+ return auth[name];
152
+ },
153
+ async fileExists(filePath) {
154
+ if (filePath !== auth.GOOGLE_APPLICATION_CREDENTIALS) return false;
155
+ try {
156
+ await access(filePath);
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ },
162
+ };
163
+ scopePiProviderEnvironment({
164
+ modelRuntime,
165
+ authenticationEnvironment: auth,
166
+ });
167
+ isolatedCredentials.finishInitialization();
168
+ await modelRuntime.refresh({ allowNetwork: false });
169
+
170
+ return modelRuntime;
171
+ }
172
+
49
173
  function createGatewayProviderConfig({
50
174
  apiKey,
51
175
  baseUrl,
@@ -86,68 +210,54 @@ async function register({
86
210
  await registries.modelRuntime.setRuntimeApiKey(provider, apiKey);
87
211
  }
88
212
 
89
- function hasConfiguredValue(value: unknown): boolean {
90
- if (value == null) return false;
91
- if (typeof value === 'string') return value.length > 0;
92
- if (typeof value !== 'object') return true;
93
- return Object.values(value).some(hasConfiguredValue);
94
- }
95
-
96
213
  export function resolvePiEnv({
97
214
  options,
98
215
  env,
99
216
  }: {
100
- options: PiAuthOptions | undefined;
217
+ options: PiAuthenticationMode | undefined;
101
218
  env: NodeJS.ProcessEnv;
102
219
  }): Record<string, string> {
103
- const normalizedOptions = normalizePiAuthToLegacyAuth(options);
104
- const customEnvConfigured = hasConfiguredValue(normalizedOptions?.customEnv);
105
- if (customEnvConfigured) {
106
- return resolveCustomEnv({ customEnv: normalizedOptions!.customEnv ?? {} });
107
- }
108
-
109
- const gatewayConfigured = hasConfiguredValue(normalizedOptions?.gateway);
110
- const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env });
111
- if (gatewayConfigured) {
112
- const apiKey =
113
- normalizedOptions!.gateway?.apiKey ?? gatewayAuthFromEnv.apiKey;
114
- const baseUrl =
115
- normalizedOptions!.gateway?.baseUrl ?? gatewayAuthFromEnv.baseUrl;
116
- if (apiKey) {
117
- return { AI_GATEWAY_API_KEY: apiKey, AI_GATEWAY_BASE_URL: baseUrl };
118
- }
119
- return {};
120
- }
220
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(options);
221
+ const authenticationEnvironment = suppliedEnvironment ? options : env;
222
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
223
+ env: authenticationEnvironment,
224
+ });
121
225
 
122
226
  // Handle explicit string modes with process env
123
227
  if (typeof options === 'string') {
124
228
  switch (options) {
125
229
  case 'openai':
126
- if (env.OPENAI_API_KEY) {
230
+ if (authenticationEnvironment.OPENAI_API_KEY) {
127
231
  return {
128
- OPENAI_API_KEY: env.OPENAI_API_KEY,
129
- ...(env.OPENAI_BASE_URL
130
- ? { OPENAI_BASE_URL: env.OPENAI_BASE_URL }
232
+ OPENAI_API_KEY: authenticationEnvironment.OPENAI_API_KEY,
233
+ ...(authenticationEnvironment.OPENAI_BASE_URL
234
+ ? { OPENAI_BASE_URL: authenticationEnvironment.OPENAI_BASE_URL }
131
235
  : {}),
132
236
  };
133
237
  }
134
238
  return {};
135
239
  case 'anthropic':
136
- if (env.ANTHROPIC_API_KEY) {
240
+ if (authenticationEnvironment.ANTHROPIC_API_KEY) {
137
241
  return {
138
- ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
139
- ...(env.ANTHROPIC_BASE_URL
140
- ? { ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL }
242
+ ANTHROPIC_API_KEY: authenticationEnvironment.ANTHROPIC_API_KEY,
243
+ ...(authenticationEnvironment.ANTHROPIC_BASE_URL
244
+ ? {
245
+ ANTHROPIC_BASE_URL:
246
+ authenticationEnvironment.ANTHROPIC_BASE_URL,
247
+ }
141
248
  : {}),
142
- ...(env.ANTHROPIC_AUTH_TOKEN
143
- ? { ANTHROPIC_AUTH_TOKEN: env.ANTHROPIC_AUTH_TOKEN }
249
+ ...(authenticationEnvironment.ANTHROPIC_AUTH_TOKEN
250
+ ? {
251
+ ANTHROPIC_AUTH_TOKEN:
252
+ authenticationEnvironment.ANTHROPIC_AUTH_TOKEN,
253
+ }
144
254
  : {}),
145
255
  };
146
256
  }
147
257
  return {};
148
258
  case 'custom': {
149
259
  const result: Record<string, string> = {};
150
- for (const [key, value] of Object.entries(env)) {
260
+ for (const [key, value] of Object.entries(authenticationEnvironment)) {
151
261
  if (
152
262
  value &&
153
263
  (key.endsWith('_API_KEY') ||
@@ -183,7 +293,7 @@ export function resolvePiEnv({
183
293
 
184
294
  // 'auto' fallback: pick up any other provider credentials from the env.
185
295
  const ambient: Record<string, string> = {};
186
- for (const [key, value] of Object.entries(env)) {
296
+ for (const [key, value] of Object.entries(authenticationEnvironment)) {
187
297
  if (
188
298
  value &&
189
299
  (key.endsWith('_API_KEY') ||
@@ -202,32 +312,20 @@ export async function registerPiProviders({
202
312
  registries,
203
313
  clientApp = HARNESS_CLIENT_APP,
204
314
  }: {
205
- options: PiAuthOptions | undefined;
315
+ options: PiAuthenticationMode | undefined;
206
316
  resolvedEnv: Record<string, string>;
207
317
  registries: PiRegistries;
208
318
  clientApp?: string;
209
319
  }): Promise<void> {
210
- const normalizedOptions = normalizePiAuthToLegacyAuth(options);
211
- if (hasConfiguredValue(normalizedOptions?.customEnv)) {
212
- await registerCustomProviders({
213
- customEnv: normalizedOptions!.customEnv ?? {},
214
- registries,
215
- clientApp,
216
- });
217
- return;
218
- }
219
-
220
- // Legacy customEnv was handled above. Everything else reduces to a mode:
221
- // string modes pass through, `undefined` is 'auto', and legacy gateway
222
- // objects fall through to the trailing gateway-registration block.
223
- const mode =
224
- typeof options === 'string' ? options : options == null ? 'auto' : 'legacy';
320
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(options);
321
+ const authenticationEnvironment = suppliedEnvironment ? options : process.env;
322
+ const mode = typeof options === 'string' ? options : 'auto';
225
323
 
226
324
  switch (mode) {
227
325
  case 'openai': {
228
326
  const env = pickOpenAIEnv(resolvedEnv);
229
327
  await registerCustomProviders({
230
- customEnv: { ...pickOpenAIEnv(process.env), ...env },
328
+ customEnv: { ...pickOpenAIEnv(authenticationEnvironment), ...env },
231
329
  registries,
232
330
  clientApp,
233
331
  });
@@ -236,7 +334,7 @@ export async function registerPiProviders({
236
334
  case 'anthropic': {
237
335
  const env = pickAnthropicEnv(resolvedEnv);
238
336
  await registerCustomProviders({
239
- customEnv: { ...pickAnthropicEnv(process.env), ...env },
337
+ customEnv: { ...pickAnthropicEnv(authenticationEnvironment), ...env },
240
338
  registries,
241
339
  clientApp,
242
340
  });
@@ -246,14 +344,16 @@ export async function registerPiProviders({
246
344
  // 'custom' registers every provider with credentials in the env.
247
345
  const env = pickProviderEnv(resolvedEnv);
248
346
  await registerCustomProviders({
249
- customEnv: { ...pickProviderEnv(process.env), ...env },
347
+ customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
250
348
  registries,
251
349
  clientApp,
252
350
  });
253
351
  return;
254
352
  }
255
353
  case 'ai-gateway': {
256
- const gatewayAuth = getAiGatewayAuthFromEnv({ env: process.env });
354
+ const gatewayAuth = getAiGatewayAuthFromEnv({
355
+ env: authenticationEnvironment,
356
+ });
257
357
  const gatewayApiKey =
258
358
  resolvedEnv.AI_GATEWAY_API_KEY ?? gatewayAuth.apiKey;
259
359
  const gatewayBaseUrl =
@@ -271,13 +371,13 @@ export async function registerPiProviders({
271
371
  });
272
372
  return;
273
373
  }
274
- case 'legacy':
275
- break; // handled below
276
374
  case 'auto':
277
375
  default: {
278
376
  // 'auto' (the default): prefer the AI Gateway; only when no gateway
279
377
  // credentials exist, fall back to other providers found in the env.
280
- const gatewayAuth = getAiGatewayAuthFromEnv({ env: process.env });
378
+ const gatewayAuth = getAiGatewayAuthFromEnv({
379
+ env: authenticationEnvironment,
380
+ });
281
381
  const gatewayApiKey =
282
382
  resolvedEnv.AI_GATEWAY_API_KEY ?? gatewayAuth.apiKey;
283
383
  const gatewayBaseUrl =
@@ -297,25 +397,13 @@ export async function registerPiProviders({
297
397
  }
298
398
  const env = pickProviderEnv(resolvedEnv);
299
399
  await registerCustomProviders({
300
- customEnv: { ...pickProviderEnv(process.env), ...env },
400
+ customEnv: { ...pickProviderEnv(authenticationEnvironment), ...env },
301
401
  registries,
302
402
  clientApp,
303
403
  });
304
404
  return;
305
405
  }
306
406
  }
307
-
308
- // Legacy explicit gateway object options.
309
- const apiKey = resolvedEnv.AI_GATEWAY_API_KEY;
310
- const baseUrl = resolvedEnv.AI_GATEWAY_BASE_URL;
311
- if (!apiKey || !baseUrl) return;
312
-
313
- await register({
314
- registries,
315
- provider: 'vercel-ai-gateway',
316
- apiKey,
317
- config: createGatewayProviderConfig({ apiKey, baseUrl, clientApp }),
318
- });
319
407
  }
320
408
 
321
409
  function pickOpenAIEnv(
@@ -363,46 +451,6 @@ function pickProviderEnv(
363
451
  return result;
364
452
  }
365
453
 
366
- function normalizePiAuthToLegacyAuth(
367
- options: PiAuthOptions | undefined,
368
- ): LegacyPiAuthOptions | undefined {
369
- if (options == null || options === 'auto') {
370
- return undefined;
371
- }
372
- if (typeof options === 'string') {
373
- switch (options) {
374
- case 'ai-gateway':
375
- return { gateway: {} };
376
- case 'custom':
377
- case 'openai':
378
- case 'anthropic':
379
- return { customEnv: {} };
380
- default:
381
- return undefined;
382
- }
383
- }
384
-
385
- console.warn(
386
- '[pi] Passing an object to auth options is deprecated. Use a string mode ("auto" | "openai" | "anthropic" | "custom" | "ai-gateway") instead, and pass credentials via environment variables.',
387
- );
388
- return options;
389
- }
390
-
391
- function resolveCustomEnv({
392
- customEnv,
393
- }: {
394
- customEnv: Record<string, string>;
395
- }): Record<string, string> {
396
- const apiKey = customEnv.AI_GATEWAY_API_KEY;
397
- if (!apiKey) return {};
398
-
399
- return {
400
- AI_GATEWAY_API_KEY: apiKey,
401
- AI_GATEWAY_BASE_URL:
402
- customEnv.AI_GATEWAY_BASE_URL ?? DEFAULT_GATEWAY_BASE_URL,
403
- };
404
- }
405
-
406
454
  async function registerCustomProviders({
407
455
  customEnv,
408
456
  registries,
package/src/pi-harness.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  import { tool } from '@ai-sdk/provider-utils';
7
7
  import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
8
8
  import { z } from 'zod/v4';
9
- import type { PiAuthOptions } from './pi-auth';
9
+ import type { PiAuthenticationMode } from './pi-auth';
10
10
  import { piResumeStateSchema } from './pi-resume-state';
11
11
  import { createPiSession, type PiThinkingLevel } from './pi-session';
12
12
  import { VERSION } from './version';
@@ -22,7 +22,7 @@ const PI_CLIENT_APP = `ai-sdk/harness-pi/${VERSION}`;
22
22
  */
23
23
  export type PiHarnessSettings = {
24
24
  /** Where Pi sources API keys / gateway credentials from. */
25
- readonly auth?: PiAuthOptions;
25
+ readonly auth?: PiAuthenticationMode;
26
26
  /**
27
27
  * Pi model id (or name). Leaving this unset falls back to the AI Gateway
28
28
  * default when `AI_GATEWAY_API_KEY` / `VERCEL_OIDC_TOKEN` is set, and to
package/src/pi-session.ts CHANGED
@@ -3,7 +3,6 @@ import {
3
3
  DefaultResourceLoader,
4
4
  defineTool,
5
5
  ModelRegistry,
6
- ModelRuntime,
7
6
  SessionManager,
8
7
  SettingsManager,
9
8
  type AgentSession,
@@ -37,9 +36,10 @@ import {
37
36
  } from '@ai-sdk/harness/utils';
38
37
  import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
39
38
  import {
39
+ createPiModelRuntime,
40
40
  registerPiProviders,
41
41
  resolvePiEnv,
42
- type PiAuthOptions,
42
+ type PiAuthenticationMode,
43
43
  } from './pi-auth';
44
44
  import { getPiTerminalError, parseNativeEvent } from './pi-events';
45
45
  import { createPiModelResolver } from './pi-model-resolver';
@@ -215,7 +215,7 @@ export type PiThinkingLevel =
215
215
  | 'xhigh';
216
216
 
217
217
  export interface PiSessionSettings {
218
- readonly auth?: PiAuthOptions;
218
+ readonly auth?: PiAuthenticationMode;
219
219
  readonly model?: string;
220
220
  readonly thinkingLevel?: PiThinkingLevel;
221
221
  readonly mcpServers?: Record<string, unknown>;
@@ -393,11 +393,16 @@ export async function createPiSession(
393
393
  // real host filesystem, never in the sandbox/workspace.
394
394
  // When `agentDir` is provided, use it instead so the harness can reuse
395
395
  // existing CLI logins and model/settings config.
396
+ /*
397
+ * A record-shaped authentication override makes createPiModelRuntime ignore
398
+ * auth.json and models.json because both files can supply credentials from
399
+ * outside that record. General Pi settings still use agentDir below.
400
+ */
396
401
  const agentDir = input.agentDir ?? hostAgentDir;
397
- const modelRuntime = await ModelRuntime.create({
402
+ const modelRuntime = await createPiModelRuntime({
403
+ auth: input.settings.auth,
398
404
  authPath: path.join(agentDir, 'auth.json'),
399
405
  modelsPath: path.join(agentDir, 'models.json'),
400
- allowModelNetwork: false,
401
406
  });
402
407
  const modelRegistry = new ModelRegistry(modelRuntime);
403
408
  const settingsManager =