@nijaru/pi-search 0.0.1 → 0.0.2

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,11 +1,10 @@
1
1
  {
2
2
  "name": "@nijaru/pi-search",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Provider-neutral web search and fetching tools for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Nick Naru <nick@nijaru.com>",
8
-
9
8
  "publishConfig": {
10
9
  "access": "public"
11
10
  },
@@ -42,16 +41,16 @@
42
41
  "live:smoke": "bun scripts/live-smoke.ts"
43
42
  },
44
43
  "devDependencies": {
45
- "@earendil-works/pi-ai": "0.85.1",
46
- "@earendil-works/pi-coding-agent": "0.85.1",
47
- "@earendil-works/pi-tui": "0.85.1",
44
+ "@earendil-works/pi-ai": "0.87.0",
45
+ "@earendil-works/pi-coding-agent": "0.87.0",
46
+ "@earendil-works/pi-tui": "0.87.0",
48
47
  "@types/bun": "1.3.14",
49
48
  "@types/turndown": "^5.0.6",
50
49
  "typebox": "1.3.10",
51
50
  "typescript": "7.0.2"
52
51
  },
53
52
  "peerDependencies": {
54
- "@earendil-works/pi-ai": "^0.85.0",
53
+ "@earendil-works/pi-ai": "*",
55
54
  "@earendil-works/pi-coding-agent": "*",
56
55
  "@earendil-works/pi-tui": "*",
57
56
  "typebox": "*"
package/src/anthropic.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  tokenUsage,
19
19
  type GroundingPlan,
20
20
  } from "./grounding";
21
- import { modelAuthHeaders, type ModelExecution } from "./model-selection";
21
+ import { hasExplicitHeader, modelAuthHeaders, type ModelExecution } from "./model-selection";
22
22
  import { httpSource, objectValue, optionalString, type SearchHttpFetch } from "./provider-http";
23
23
  import { validateSearchRequest } from "./search";
24
24
 
@@ -125,10 +125,17 @@ function endpointFor(model: ProviderModel, override?: string): string {
125
125
  function anthropicHeaders(execution: ModelExecution): Readonly<Record<string, string>> {
126
126
  const headers = modelAuthHeaders(execution, { bearerApiKey: false });
127
127
  const apiKey = execution.auth.apiKey;
128
- if (apiKey === undefined || apiKey.trim().length === 0) {
128
+ // An explicit x-api-key (value or null deletion) wins over a derived key.
129
+ if (
130
+ apiKey !== undefined &&
131
+ apiKey.trim().length > 0 &&
132
+ !hasExplicitHeader([execution.model.headers, execution.auth.headers], "x-api-key")
133
+ ) {
134
+ headers.set("x-api-key", apiKey);
135
+ }
136
+ if (!headers.has("x-api-key")) {
129
137
  throw createProviderError({ provider: "anthropic", kind: "auth", message: "Anthropic authentication returned no API key", retryable: false });
130
138
  }
131
- headers.set("x-api-key", apiKey);
132
139
  headers.set("anthropic-version", ANTHROPIC_VERSION);
133
140
  return Object.fromEntries(headers.entries());
134
141
  }
package/src/contracts.ts CHANGED
@@ -55,6 +55,10 @@ export type ProviderAuthResult =
55
55
  readonly ok: true;
56
56
  readonly apiKey?: string;
57
57
  readonly headers?: ProviderHeaders;
58
+ /** Provider-resolved endpoint override; wins over the catalog `baseUrl`. */
59
+ readonly baseUrl?: string;
60
+ /** Provider-scoped environment credentials resolved with the request. */
61
+ readonly env?: Readonly<Record<string, string>>;
58
62
  }
59
63
  | {
60
64
  readonly ok: false;
package/src/gemini.ts CHANGED
@@ -14,7 +14,7 @@ import { createProviderError } from "./errors";
14
14
  import { cancelResponseBody } from "./http";
15
15
  import { executeGroundedSearch } from "./grounding";
16
16
  import { httpSource, objectValue, optionalString, type SearchHttpFetch } from "./provider-http";
17
- import { modelAuthHeaders, type ModelExecution } from "./model-selection";
17
+ import { hasExplicitHeader, modelAuthHeaders, type ModelExecution } from "./model-selection";
18
18
  import { validateSearchRequest } from "./search";
19
19
 
20
20
  export const GEMINI_GENERATE_CONTENT_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta";
@@ -88,7 +88,14 @@ function endpointFor(model: ProviderModel, override?: string): string {
88
88
 
89
89
  function authHeaders(execution: ModelExecution): Readonly<Record<string, string>> {
90
90
  const headers = modelAuthHeaders(execution, { bearerApiKey: false });
91
- if (execution.auth.apiKey !== undefined && execution.auth.apiKey.trim().length > 0) headers.set("x-goog-api-key", execution.auth.apiKey);
91
+ // An explicit x-goog-api-key (value or null deletion) wins over a derived key.
92
+ if (
93
+ execution.auth.apiKey !== undefined &&
94
+ execution.auth.apiKey.trim().length > 0 &&
95
+ !hasExplicitHeader([execution.model.headers, execution.auth.headers], "x-goog-api-key")
96
+ ) {
97
+ headers.set("x-goog-api-key", execution.auth.apiKey);
98
+ }
92
99
  if (!headers.has("x-goog-api-key")) {
93
100
  throw createProviderError({ provider: "gemini", kind: "auth", message: "Gemini authentication returned no API key", retryable: false });
94
101
  }
@@ -109,7 +109,9 @@ async function authenticate(model: ProviderModel, registry: NonNullable<Provider
109
109
  if (!auth.ok) {
110
110
  throw createProviderError({ provider: options.searchProvider, kind: "auth", message: `Pi model authentication is not configured for ${model.id}`, retryable: false });
111
111
  }
112
- return { model, auth };
112
+ // A provider-resolved endpoint wins over the catalog base URL, matching
113
+ // `ModelRegistry.stream()`; adapters build their request URL from this model.
114
+ return { model: auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model, auth };
113
115
  }
114
116
 
115
117
  export interface ModelAuthHeaderOptions {
@@ -125,10 +127,26 @@ export function applyProviderHeaders(headers: Headers, source: ProviderHeaders |
125
127
  }
126
128
  }
127
129
 
130
+ /**
131
+ * Whether any source declares a header, including a `null` deletion. An explicit
132
+ * `authorization` entry must win over a bearer synthesized from the API key.
133
+ */
134
+ export function hasExplicitHeader(sources: readonly (ProviderHeaders | undefined)[], name: string): boolean {
135
+ const lower = name.toLowerCase();
136
+ return sources.some((source) => source !== undefined && Object.keys(source).some((key) => key.toLowerCase() === lower));
137
+ }
138
+
128
139
  export function modelAuthHeaders(execution: ModelExecution, options: ModelAuthHeaderOptions = {}): Headers {
129
140
  const headers = new Headers();
130
141
  applyProviderHeaders(headers, execution.model.headers);
131
142
  applyProviderHeaders(headers, execution.auth.headers);
132
- if (options.bearerApiKey !== false && execution.auth.apiKey !== undefined && execution.auth.apiKey.trim().length > 0) headers.set("authorization", `Bearer ${execution.auth.apiKey}`);
143
+ if (
144
+ options.bearerApiKey !== false &&
145
+ execution.auth.apiKey !== undefined &&
146
+ execution.auth.apiKey.trim().length > 0 &&
147
+ !hasExplicitHeader([execution.model.headers, execution.auth.headers], "authorization")
148
+ ) {
149
+ headers.set("authorization", `Bearer ${execution.auth.apiKey}`);
150
+ }
133
151
  return headers;
134
152
  }
package/src/openai.ts CHANGED
@@ -14,7 +14,7 @@ import type {
14
14
  SearchWarning,
15
15
  } from "./contracts";
16
16
  import { createProviderError, isProviderError } from "./errors";
17
- import { applyProviderHeaders } from "./model-selection";
17
+ import { applyProviderHeaders, hasExplicitHeader } from "./model-selection";
18
18
  import { cancelResponseBody, readBoundedResponseText } from "./http";
19
19
  import { parseProviderRateLimits } from "./provider-http";
20
20
  import { validateSearchRequest } from "./search";
@@ -444,7 +444,9 @@ async function selectSearchExecution(active: ProviderModel | undefined, registry
444
444
  } catch (error) {
445
445
  throw createProviderError({ provider, kind: "auth", message: "Pi model authentication could not be resolved", retryable: false, cause: error });
446
446
  }
447
- if (auth.ok) return { model: selected, auth };
447
+ // A provider-resolved endpoint wins over the catalog base URL; the adapter
448
+ // builds its request URL from this model. Mirrors selectModelExecution().
449
+ if (auth.ok) return { model: auth.baseUrl ? { ...selected, baseUrl: auth.baseUrl } : selected, auth };
448
450
  if (request.executionModel !== undefined) {
449
451
  throw createProviderError({ provider, kind: "auth", message: `Pi model authentication is not configured for ${selected.id}`, retryable: false });
450
452
  }
@@ -776,7 +778,11 @@ export class OpenAIProvider implements Provider {
776
778
  const headers = new Headers();
777
779
  applyProviderHeaders(headers, execution.model.headers);
778
780
  applyProviderHeaders(headers, execution.auth.headers);
779
- if (execution.auth.apiKey !== undefined && execution.auth.apiKey.trim().length > 0) {
781
+ if (
782
+ execution.auth.apiKey !== undefined &&
783
+ execution.auth.apiKey.trim().length > 0 &&
784
+ !hasExplicitHeader([execution.model.headers, execution.auth.headers], "authorization")
785
+ ) {
780
786
  headers.set("Authorization", `Bearer ${execution.auth.apiKey}`);
781
787
  }
782
788
  if (!headers.has("authorization")) {
@@ -174,7 +174,13 @@ export function providerContextFromPi(context: ExtensionContext): ProviderContex
174
174
  if (selected === undefined) return { ok: false, error: `Pi model ${requested.provider}/${requested.id} is not available` };
175
175
  const resolved = await context.modelRegistry.getApiKeyAndHeaders(selected);
176
176
  return resolved.ok
177
- ? { ok: true, ...(resolved.apiKey === undefined ? {} : { apiKey: resolved.apiKey }), ...(resolved.headers === undefined ? {} : { headers: resolved.headers }) }
177
+ ? {
178
+ ok: true,
179
+ ...(resolved.apiKey === undefined ? {} : { apiKey: resolved.apiKey }),
180
+ ...(resolved.headers === undefined ? {} : { headers: resolved.headers }),
181
+ ...(resolved.baseUrl === undefined ? {} : { baseUrl: resolved.baseUrl }),
182
+ ...(resolved.env === undefined ? {} : { env: resolved.env }),
183
+ }
178
184
  : { ok: false, error: resolved.error };
179
185
  },
180
186
  },