@oh-my-pi/pi-ai 17.2.5 → 17.2.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.6] - 2026-08-03
6
+
7
+ ### Added
8
+
9
+ - Added profile-aware Bedrock Mantle region selection, authenticated model discovery, bearer-token or SigV4 authentication, and credential refresh handling for OpenAI Responses models.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed an issue where Ollama requests without a user-role message would fail to generate output or silently fail with a misleading error.
14
+
5
15
  ## [17.2.5] - 2026-08-03
6
16
 
7
17
  ### Changed
@@ -8,3 +8,11 @@ import type { InbandTool } from "./types.js";
8
8
  * tool, and payload args (commands, code, patches) read best verbatim.
9
9
  */
10
10
  export declare function renderToolExamples(tool: InbandTool, intentField?: string): string;
11
+ /**
12
+ * Render a tool's examples as JSDoc-style `@example` lines for comment-gutter
13
+ * contexts (the Harmony `namespace functions` inventory): `@example "caption"`
14
+ * followed by the call in the same Python kwargs syntax as the wire block. The
15
+ * tag line delimits each example, so no XML envelope is needed — which is why
16
+ * the inventory uses this instead of `//`-prefixing the `<examples>` block.
17
+ */
18
+ export declare function renderToolExamplesJsdoc(tool: InbandTool): string;
@@ -9,7 +9,11 @@ export type AwsCredentialsErrorKind =
9
9
  /** SSO `GetRoleCredentials` call failed or returned no role. */
10
10
  | "sso-role"
11
11
  /** External `credential_process` failed, timed out, or emitted bad output. */
12
- | "credential-process";
12
+ | "credential-process"
13
+ /** STS web-identity exchange failed or returned malformed credentials. */
14
+ | "web-identity"
15
+ /** ECS/container credential endpoint failed or returned malformed credentials. */
16
+ | "container";
13
17
  /** A failure resolving AWS credentials for the Bedrock provider. */
14
18
  export declare class AwsCredentialsError extends Error {
15
19
  readonly kind: AwsCredentialsErrorKind;
@@ -4,16 +4,11 @@
4
4
  * Chain (first hit wins):
5
5
  * 1. Static credentials from the environment
6
6
  * (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` [+ `AWS_SESSION_TOKEN`]).
7
- * 2. Profile in `~/.aws/credentials` (and `~/.aws/config` for SSO):
8
- * - static `aws_access_key_id` / `aws_secret_access_key` / `aws_session_token`
9
- * - SSO profile referencing a cached token in `~/.aws/sso/cache/*.json`,
10
- * which we exchange for short-lived role credentials via
11
- * `https://portal.sso.{region}.amazonaws.com/federation/credentials`.
12
- * - `credential_process` — an external command emitting the AWS SDK
13
- * `Version: 1` JSON envelope on stdout. Used by `aws-vault`, `granted`,
14
- * in-house brokers, etc.
15
- * 3. EC2 IMDSv2 (only when `AWS_EC2_METADATA_DISABLED` is unset / falsey and
16
- * `169.254.169.254` is reachable within a 1 s timeout).
7
+ * 2. Web identity (`AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN`).
8
+ * 3. Profile in `~/.aws/credentials` (and `~/.aws/config` for SSO):
9
+ * - static keys, SSO, or `credential_process`.
10
+ * 4. ECS/container credentials from `AWS_CONTAINER_CREDENTIALS_*`.
11
+ * 5. EC2 IMDSv2 when metadata is enabled.
17
12
  *
18
13
  * Resolved credentials are cached process-wide per profile and refreshed
19
14
  * 60 s before `Expiration` to absorb clock skew.
@@ -0,0 +1,13 @@
1
+ import { type AwsBedrockProviderOptions } from "../registry/aws.js";
2
+ import type { FetchImpl, Model } from "../types.js";
3
+ import type { OpenAIResponsesOptions } from "./openai-responses.js";
4
+ export type BedrockMantleProviderOptions = AwsBedrockProviderOptions;
5
+ export interface BedrockMantleOptions extends OpenAIResponsesOptions {
6
+ providerOptions?: BedrockMantleProviderOptions;
7
+ }
8
+ export declare function createBedrockMantleAuthenticatedFetch(options?: BedrockMantleOptions): FetchImpl;
9
+ export interface PreparedBedrockMantleRequest {
10
+ model: Model<"openai-responses">;
11
+ options: OpenAIResponsesOptions;
12
+ }
13
+ export declare function prepareBedrockMantleRequest(model: Model<"openai-responses">, options: BedrockMantleOptions): PreparedBedrockMantleRequest;
@@ -1,5 +1,11 @@
1
+ import { resolveAwsRegistryApiKey } from "./aws.js";
1
2
  export declare const amazonBedrockProvider: {
2
3
  readonly id: "amazon-bedrock";
3
4
  readonly name: "Amazon Bedrock";
4
- readonly envKeys: () => "<authenticated>" | undefined;
5
+ readonly envKeys: typeof resolveAwsRegistryApiKey;
6
+ readonly mapSimpleOptions: (options: import("../index.js").SimpleStreamOptions) => {
7
+ region: string | undefined;
8
+ profile: string | undefined;
9
+ bearerToken: string | undefined;
10
+ };
5
11
  };
@@ -0,0 +1,13 @@
1
+ export interface AwsBedrockProviderOptions extends Readonly<Record<string, unknown>> {
2
+ /** AWS region used in the service endpoint and SigV4 credential scope. */
3
+ region?: string;
4
+ /** Named AWS shared-credentials/config profile. */
5
+ profile?: string;
6
+ /** Amazon Bedrock API key sent as a bearer token, ahead of SigV4 credential resolution. */
7
+ bearerToken?: string;
8
+ }
9
+ export declare function hasAwsCredentialSource(): boolean;
10
+ /** Registry key marker for AWS transports that resolve their own bearer/IAM credentials. */
11
+ export declare function resolveAwsRegistryApiKey(): string | undefined;
12
+ /** Resolve a real AWS bearer token while filtering the registry's auth marker. */
13
+ export declare function resolveAwsBearerToken(apiKey?: string, bearerToken?: string): string | undefined;
@@ -0,0 +1,22 @@
1
+ import type { Model } from "../types.js";
2
+ import { resolveAwsRegistryApiKey } from "./aws.js";
3
+ export declare const bedrockMantleProvider: {
4
+ readonly id: "bedrock-mantle";
5
+ readonly name: "Amazon Bedrock Mantle";
6
+ readonly envKeys: typeof resolveAwsRegistryApiKey;
7
+ readonly allowsMissingApiKey: true;
8
+ readonly prepareRequest: (model: Model<import("@oh-my-pi/pi-catalog").Api>, options: import("../index.js").StreamOptions) => import("../providers/bedrock-mantle.js").PreparedBedrockMantleRequest;
9
+ readonly mapSimpleOptions: (options: import("../index.js").SimpleStreamOptions) => {
10
+ providerOptions: Readonly<Record<string, unknown>> | undefined;
11
+ };
12
+ readonly prepareModelDiscovery: (config: import("./types.js").ProviderModelDiscoveryConfig) => {
13
+ baseUrl?: string;
14
+ fetch?: import("@oh-my-pi/pi-utils").FetchImpl;
15
+ apiKey: undefined;
16
+ authenticated: false;
17
+ } | {
18
+ authenticated: true;
19
+ baseUrl: string;
20
+ fetch: import("@oh-my-pi/pi-utils").FetchImpl;
21
+ };
22
+ };
@@ -25,7 +25,12 @@ declare const ALL: ({
25
25
  } | {
26
26
  readonly id: "amazon-bedrock";
27
27
  readonly name: "Amazon Bedrock";
28
- readonly envKeys: () => "<authenticated>" | undefined;
28
+ readonly envKeys: typeof import("./aws.js").resolveAwsRegistryApiKey;
29
+ readonly mapSimpleOptions: (options: import("../index.js").SimpleStreamOptions) => {
30
+ region: string | undefined;
31
+ profile: string | undefined;
32
+ bearerToken: string | undefined;
33
+ };
29
34
  } | {
30
35
  readonly id: "anthropic";
31
36
  readonly name: "Anthropic (Claude Pro/Max)";
@@ -41,6 +46,25 @@ declare const ALL: ({
41
46
  readonly id: "baseten";
42
47
  readonly name: "Baseten";
43
48
  readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
49
+ } | {
50
+ readonly id: "bedrock-mantle";
51
+ readonly name: "Amazon Bedrock Mantle";
52
+ readonly envKeys: typeof import("./aws.js").resolveAwsRegistryApiKey;
53
+ readonly allowsMissingApiKey: true;
54
+ readonly prepareRequest: (model: import("@oh-my-pi/pi-catalog").Model<import("@oh-my-pi/pi-catalog").Api>, options: import("../index.js").StreamOptions) => import("../providers/bedrock-mantle.js").PreparedBedrockMantleRequest;
55
+ readonly mapSimpleOptions: (options: import("../index.js").SimpleStreamOptions) => {
56
+ providerOptions: Readonly<Record<string, unknown>> | undefined;
57
+ };
58
+ readonly prepareModelDiscovery: (config: import("./types.js").ProviderModelDiscoveryConfig) => {
59
+ baseUrl?: string;
60
+ fetch?: import("@oh-my-pi/pi-utils").FetchImpl;
61
+ apiKey: undefined;
62
+ authenticated: false;
63
+ } | {
64
+ authenticated: true;
65
+ baseUrl: string;
66
+ fetch: import("@oh-my-pi/pi-utils").FetchImpl;
67
+ };
44
68
  } | {
45
69
  readonly id: "cerebras";
46
70
  readonly name: "Cerebras";
@@ -9,6 +9,7 @@
9
9
  * (default model, model-manager factory, catalog discovery) lives in
10
10
  * `@oh-my-pi/pi-catalog`'s descriptor table.
11
11
  */
12
+ import type { Api, FetchImpl, Model, SimpleStreamOptions, StreamOptions } from "../types.js";
12
13
  import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types.js";
13
14
  /**
14
15
  * API-key environment fallback: either a single env var name (e.g.
@@ -16,6 +17,21 @@ import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types.js";
16
17
  * the host (Vertex ADC, Bedrock credential chains, …).
17
18
  */
18
19
  export type KeyResolver = string | (() => string | undefined);
20
+ /** Credentials are resolved by the provider transport rather than used as a bearer string. */
21
+ export declare const AUTHENTICATED_SENTINEL = "<authenticated>";
22
+ export interface PreparedProviderRequest {
23
+ readonly model: Model<Api>;
24
+ readonly options: StreamOptions;
25
+ }
26
+ export type ProviderRequestPreparer = (model: Model<Api>, options: StreamOptions) => PreparedProviderRequest;
27
+ export type ProviderSimpleOptionsMapper = (options: SimpleStreamOptions) => Readonly<Record<string, unknown>>;
28
+ export interface ProviderModelDiscoveryConfig {
29
+ readonly apiKey?: string;
30
+ readonly baseUrl?: string;
31
+ readonly fetch?: FetchImpl;
32
+ readonly authenticated?: boolean;
33
+ }
34
+ export type ProviderModelDiscoveryPreparer = (config: ProviderModelDiscoveryConfig) => ProviderModelDiscoveryConfig;
19
35
  /**
20
36
  * Declarative description of a single provider's auth/login wiring. All
21
37
  * fields are optional except `id`/`name`; presence of a field opts the
@@ -39,6 +55,14 @@ export interface ProviderDefinition {
39
55
  /** Whether to surface in the interactive login list. Defaults to true when `login` is present. */
40
56
  readonly showInLoginList?: boolean;
41
57
  readonly envKeys?: KeyResolver;
58
+ /** Provider transport can authenticate without a resolved API-key string. */
59
+ readonly allowsMissingApiKey?: boolean;
60
+ /** Provider-owned request shaping applied before generic API dispatch. */
61
+ readonly prepareRequest?: ProviderRequestPreparer;
62
+ /** Provider-owned projection from the generic simple-stream option bag. */
63
+ readonly mapSimpleOptions?: ProviderSimpleOptionsMapper;
64
+ /** Provider-owned authentication and endpoint setup for model discovery. */
65
+ readonly prepareModelDiscovery?: ProviderModelDiscoveryPreparer;
42
66
  readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
43
67
  readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
44
68
  readonly getApiKey?: (credentials: OAuthCredentials) => string;
@@ -249,6 +249,11 @@ export interface StreamOptions {
249
249
  * For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
250
250
  */
251
251
  metadata?: Record<string, unknown>;
252
+ /**
253
+ * Provider-owned request configuration. Provider hooks interpret this bag;
254
+ * generic API transports do not forward its fields onto the wire.
255
+ */
256
+ providerOptions?: Readonly<Record<string, unknown>>;
252
257
  /** OpenAI Responses/Codex response fields to include verbatim. */
253
258
  include?: OpenAIResponseInclude[];
254
259
  /**
@@ -0,0 +1,17 @@
1
+ /** INI sections with `profile ` / `sso-session ` prefixes normalized. */
2
+ export type AwsIniFile = Record<string, Record<string, string>>;
3
+ export declare function parseAwsIni(text: string): AwsIniFile;
4
+ /** Resolve the selected shared-credentials profile. */
5
+ export declare function resolveAwsProfile(profile?: string): string;
6
+ /**
7
+ * Whether the shared config file participates in profile/region resolution.
8
+ * Explicit profile selection enables it; the implicit default profile follows
9
+ * the AWS SDK's `AWS_SDK_LOAD_CONFIG` opt-in.
10
+ */
11
+ export declare function shouldLoadAwsSharedConfig(profile?: string): boolean;
12
+ export declare function resolveAwsProfileRegion(profile?: string): string | undefined;
13
+ /** Region selected by the environment or active shared-config profile. */
14
+ export declare function resolveAwsAmbientRegion(profile?: string): string | undefined;
15
+ /** Resolve the region precedence shared by AWS transports and credential exchanges. */
16
+ export declare function resolveAwsRegion(explicitRegion?: string, profile?: string): string;
17
+ export declare function hasConfiguredAwsProfile(profile?: string): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.2.5",
4
+ "version": "17.2.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/pi-catalog": "17.2.5",
42
- "@oh-my-pi/pi-utils": "17.2.5",
43
- "@oh-my-pi/pi-wire": "17.2.5",
41
+ "@oh-my-pi/pi-catalog": "17.2.6",
42
+ "@oh-my-pi/pi-utils": "17.2.6",
43
+ "@oh-my-pi/pi-wire": "17.2.6",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -12,7 +12,7 @@
12
12
  * exception (standard type keeps extra keys): it preserves provider-specific extension fields so
13
13
  * they round-trip through the broker instead of being dropped (see below).
14
14
  */
15
- import { type Type, type } from "arktype";
15
+ import { scope, type Type } from "arktype";
16
16
  import {
17
17
  type ApiKeyCredential,
18
18
  type AuthCredential,
@@ -84,6 +84,11 @@ export interface AuthBrokerWireSchemas {
84
84
  }
85
85
 
86
86
  function buildAuthBrokerWireSchemas(): AuthBrokerWireSchemas {
87
+ // Wire schemas validate only a handful of times per process, so ArkType's
88
+ // definition-time JIT codegen is startup tax. A local jitless scope skips
89
+ // that codegen and uses interpreted traversal; correctness is unchanged.
90
+ const { type } = scope({}, { jitless: true });
91
+
87
92
  // ─── Credential payloads ───────────────────────────────────────────────────
88
93
 
89
94
  /** Real OAuth credential (broker-side) — refresh token is the actual upstream value. */
@@ -15,17 +15,12 @@ export function renderToolExamples(tool: InbandTool, intentField?: string): stri
15
15
  const examples = tool.examples;
16
16
  if (!examples?.length) return "";
17
17
  const renderCall = (args: Record<string, unknown>): string => {
18
- let soleKey: string | undefined;
19
- let argCount = 0;
20
- for (const key in args) {
21
- argCount++;
22
- soleKey = key;
23
- }
24
- if (argCount === 1 && soleKey !== undefined && typeof args[soleKey] === "string") {
18
+ const bare = bareStringArg(args);
19
+ if (bare !== undefined) {
25
20
  // Bare payload. The intent placeholder still rides on the envelope so
26
21
  // intent-traced schemas (where `i` is required) keep teaching it.
27
22
  const intentAttr = intentField ? ` ${intentField}="${INTENT_PLACEHOLDER}"` : "";
28
- return `<example${intentAttr}>\n${args[soleKey]}\n</example>`;
23
+ return `<example${intentAttr}>\n${bare}\n</example>`;
29
24
  }
30
25
  // When intent tracing injects `i` into the schema, examples must show a
31
26
  // placeholder so the model learns to emit it. Keep it first, matching the
@@ -43,3 +38,34 @@ export function renderToolExamples(tool: InbandTool, intentField?: string): stri
43
38
  });
44
39
  return `<examples>\n${parts.join("\n")}\n</examples>`;
45
40
  }
41
+
42
+ /**
43
+ * Render a tool's examples as JSDoc-style `@example` lines for comment-gutter
44
+ * contexts (the Harmony `namespace functions` inventory): `@example "caption"`
45
+ * followed by the call in the same Python kwargs syntax as the wire block. The
46
+ * tag line delimits each example, so no XML envelope is needed — which is why
47
+ * the inventory uses this instead of `//`-prefixing the `<examples>` block.
48
+ */
49
+ export function renderToolExamplesJsdoc(tool: InbandTool): string {
50
+ const examples = tool.examples;
51
+ if (!examples?.length) return "";
52
+ const renderCall = (args: Record<string, unknown>): string => bareStringArg(args) ?? pyCall(tool.name, args);
53
+ const parts = examples.map(ex => {
54
+ const head = ex.caption ? `@example ${JSON.stringify(ex.caption)}` : "@example";
55
+ if ("call" in ex) return `${head}\n${renderCall(ex.call)}`;
56
+ if ("good" in ex) return `${head}\nWRONG:\n${renderCall(ex.bad)}\nRIGHT:\n${renderCall(ex.good)}`;
57
+ return ex.note ? `${head}\n${ex.note}` : head;
58
+ });
59
+ return parts.join("\n");
60
+ }
61
+
62
+ /** Sole-argument string payload, if the call has exactly one string argument. */
63
+ function bareStringArg(args: Record<string, unknown>): string | undefined {
64
+ let sole: unknown;
65
+ let count = 0;
66
+ for (const key in args) {
67
+ count++;
68
+ sole = args[key];
69
+ }
70
+ return count === 1 && typeof sole === "string" ? sole : undefined;
71
+ }
@@ -1,5 +1,5 @@
1
1
  import { jsonSchemaToTypeScript, toolWireSchema } from "../utils/schema";
2
- import { renderToolExamples } from "./examples";
2
+ import { renderToolExamplesJsdoc } from "./examples";
3
3
  import type { InbandTool } from "./types";
4
4
 
5
5
  /**
@@ -18,7 +18,7 @@ export function renderToolInventory(tools: readonly InbandTool[]): string {
18
18
  if (description) {
19
19
  for (const line of description.split("\n")) lines.push(`// ${line}`.trimEnd());
20
20
  }
21
- const examples = renderToolExamples(tool);
21
+ const examples = renderToolExamplesJsdoc(tool);
22
22
  if (examples) {
23
23
  if (description) lines.push("//");
24
24
  for (const line of examples.split("\n")) lines.push(`// ${line}`.trimEnd());
package/src/error/aws.ts CHANGED
@@ -9,7 +9,11 @@ export type AwsCredentialsErrorKind =
9
9
  /** SSO `GetRoleCredentials` call failed or returned no role. */
10
10
  | "sso-role"
11
11
  /** External `credential_process` failed, timed out, or emitted bad output. */
12
- | "credential-process";
12
+ | "credential-process"
13
+ /** STS web-identity exchange failed or returned malformed credentials. */
14
+ | "web-identity"
15
+ /** ECS/container credential endpoint failed or returned malformed credentials. */
16
+ | "container";
13
17
 
14
18
  /** A failure resolving AWS credentials for the Bedrock provider. */
15
19
  export class AwsCredentialsError extends Error {
@@ -10,9 +10,10 @@
10
10
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
11
11
  import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "@oh-my-pi/pi-catalog/model-thinking";
12
12
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
13
- import { $env, $flag, fetchWithRetry, parseStreamingJson, parseStreamingJsonThrottled } from "@oh-my-pi/pi-utils";
13
+ import { $flag, fetchWithRetry, parseStreamingJson, parseStreamingJsonThrottled } from "@oh-my-pi/pi-utils";
14
14
  import { renderDemotedThinking } from "../dialect/demotion";
15
15
  import * as AIError from "../error";
16
+ import { resolveAwsBearerToken } from "../registry/aws";
16
17
  import type {
17
18
  Api,
18
19
  AssistantMessage,
@@ -30,6 +31,7 @@ import type {
30
31
  ToolResultMessage,
31
32
  } from "../types";
32
33
  import { normalizeSystemPrompts, normalizeToolCallId, resolveCacheRetention } from "../utils";
34
+ import { resolveAwsAmbientRegion } from "../utils/aws-profile";
33
35
  import {
34
36
  clearStreamingPartialJson,
35
37
  kStreamingBlockIndex,
@@ -74,11 +76,9 @@ export interface BedrockOptions extends StreamOptions {
74
76
  */
75
77
  thinkingDisplay?: BedrockThinkingDisplay;
76
78
  }
77
- const AUTHENTICATED_API_KEY_SENTINEL = "<authenticated>";
78
79
 
79
80
  function resolveBearerToken(options: BedrockOptions): string | undefined {
80
- const apiKey = options.apiKey === AUTHENTICATED_API_KEY_SENTINEL ? undefined : options.apiKey;
81
- return options.bearerToken || apiKey || $env.AWS_BEARER_TOKEN_BEDROCK;
81
+ return resolveAwsBearerToken(options.apiKey, options.bearerToken);
82
82
  }
83
83
 
84
84
  function inferRegionFromBedrockArn(modelId: string): string | undefined {
@@ -149,7 +149,7 @@ function regionServesGeo(region: string, geo: string): boolean {
149
149
  function resolveBedrockRegion(modelId: string, options: BedrockOptions): string {
150
150
  const explicit = options.region || inferRegionFromBedrockArn(modelId);
151
151
  if (explicit) return explicit;
152
- const ambient = $env.AWS_REGION || $env.AWS_DEFAULT_REGION;
152
+ const ambient = resolveAwsAmbientRegion(options.profile);
153
153
  const geo = inferenceProfileGeo(modelId);
154
154
  if (geo) {
155
155
  if (ambient && regionServesGeo(ambient, geo)) return ambient;