@oh-my-pi/pi-ai 17.2.5 → 17.2.7
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 +21 -0
- package/dist/types/auth-broker/client.d.ts +7 -0
- package/dist/types/auth-broker/server.d.ts +11 -0
- package/dist/types/auth-broker/wire-schema-resource.d.ts +1 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +4 -2
- package/dist/types/dialect/examples.d.ts +8 -0
- package/dist/types/error/aws.d.ts +5 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +695 -255
- package/dist/types/providers/aws-credentials.d.ts +5 -10
- package/dist/types/providers/bedrock-mantle.d.ts +13 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +909 -293
- package/dist/types/providers/openai-responses-server-schema.d.ts +980 -353
- package/dist/types/registry/amazon-bedrock.d.ts +7 -1
- package/dist/types/registry/aws.d.ts +13 -0
- package/dist/types/registry/bedrock-mantle.d.ts +22 -0
- package/dist/types/registry/registry.d.ts +25 -1
- package/dist/types/registry/types.d.ts +24 -0
- package/dist/types/types.d.ts +13 -5
- package/dist/types/usage.d.ts +152 -46
- package/dist/types/utils/aws-profile.d.ts +17 -0
- package/dist/types/utils/schema/wire.d.ts +1 -1
- package/dist/types/utils/validation.d.ts +24 -0
- package/package.json +5 -5
- package/src/auth-broker/client.ts +2 -1
- package/src/auth-broker/server.ts +2 -1
- package/src/auth-broker/wire-schema-resource.ts +1 -1
- package/src/dialect/examples.ts +34 -8
- package/src/dialect/inventory.ts +2 -2
- package/src/error/aws.ts +5 -1
- package/src/index.ts +1 -1
- package/src/providers/amazon-bedrock.ts +5 -5
- package/src/providers/anthropic-messages-server-schema.ts +1 -1
- package/src/providers/anthropic-messages-server.ts +1 -1
- package/src/providers/aws-credentials.ts +262 -76
- package/src/providers/bedrock-mantle.ts +110 -0
- package/src/providers/cursor.ts +31 -3
- package/src/providers/google-gemini-cli.ts +1 -1
- package/src/providers/ollama.ts +27 -1
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +1 -1
- package/src/providers/openai-codex-responses.ts +1 -1
- package/src/providers/openai-responses-server-schema.ts +1 -1
- package/src/providers/openai-responses-server.ts +1 -1
- package/src/registry/amazon-bedrock.ts +9 -14
- package/src/registry/aws.ts +57 -0
- package/src/registry/bedrock-mantle.ts +34 -0
- package/src/registry/google-vertex.ts +2 -2
- package/src/registry/registry.ts +2 -0
- package/src/registry/types.ts +30 -0
- package/src/stream.ts +46 -30
- package/src/types.ts +14 -6
- package/src/usage.ts +1 -1
- package/src/utils/aws-profile.ts +88 -0
- package/src/utils/schema/wire.ts +1 -1
- package/src/utils/validation.ts +2 -1
package/src/providers/cursor.ts
CHANGED
|
@@ -144,6 +144,7 @@ import { isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
|
144
144
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
145
145
|
import {
|
|
146
146
|
$env,
|
|
147
|
+
logger,
|
|
147
148
|
parseJsonWithRepair,
|
|
148
149
|
parseStreamingJson,
|
|
149
150
|
parseStreamingJsonThrottled,
|
|
@@ -233,6 +234,7 @@ const NOT_IMPLEMENTED = `Not implemented by this client`;
|
|
|
233
234
|
|
|
234
235
|
const conversationStateCache = new Map<string, ConversationStateStructure>();
|
|
235
236
|
const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
|
|
237
|
+
const warnedCursorKimiK3ReplayMessages = new Set<string>();
|
|
236
238
|
|
|
237
239
|
export interface CursorOptions extends StreamOptions {
|
|
238
240
|
customSystemPrompt?: string;
|
|
@@ -3902,6 +3904,15 @@ export function processInteractionUpdate(
|
|
|
3902
3904
|
}
|
|
3903
3905
|
} else if (updateCase === "turnEnded") {
|
|
3904
3906
|
output.stopReason = "stop";
|
|
3907
|
+
if (
|
|
3908
|
+
isKimiK3ModelId(output.model) &&
|
|
3909
|
+
!output.content.some(item => item.type === "thinking" && item.thinking.length > 0)
|
|
3910
|
+
) {
|
|
3911
|
+
logger.warn(
|
|
3912
|
+
"Cursor kimi-k3 turn completed without thinking blocks; persisted history will replay this turn without reasoning",
|
|
3913
|
+
{ model: output.model, messageTimestamp: output.timestamp },
|
|
3914
|
+
);
|
|
3915
|
+
}
|
|
3905
3916
|
} else if (updateCase === "tokenDelta") {
|
|
3906
3917
|
const tokenDelta = update.message.value;
|
|
3907
3918
|
usageState.sawTokenDelta = true;
|
|
@@ -4113,17 +4124,34 @@ function assertCursorKimiK3HistoryReplayable(
|
|
|
4113
4124
|
): void {
|
|
4114
4125
|
if (!targetModelId || !isKimiK3ModelId(targetModelId)) return;
|
|
4115
4126
|
const historyEnd = activeUserMessageIndex >= 0 ? activeUserMessageIndex : messages.length;
|
|
4127
|
+
const missingThinkingTurns: number[] = [];
|
|
4128
|
+
const newlyWarnedKeys: string[] = [];
|
|
4129
|
+
let assistantTurn = 0;
|
|
4116
4130
|
for (let i = 0; i < historyEnd; i++) {
|
|
4117
4131
|
const msg = messages[i];
|
|
4118
4132
|
if (msg.role !== "assistant") continue;
|
|
4133
|
+
assistantTurn++;
|
|
4119
4134
|
const isSameCursorModel = msg.api === "cursor-agent" && msg.provider === "cursor" && msg.model === targetModelId;
|
|
4120
|
-
|
|
4121
|
-
|
|
4135
|
+
if (!isSameCursorModel) {
|
|
4136
|
+
// Foreign history genuinely cannot replay K3 thinking: another model's
|
|
4137
|
+
// turns carry no K3-signed reasoning to reconstruct.
|
|
4122
4138
|
throw new AIError.ValidationError(
|
|
4123
|
-
`Cursor ${targetModelId}
|
|
4139
|
+
`Cursor ${targetModelId} cannot continue history from a different model (${msg.provider}/${msg.model}); start a new session.`,
|
|
4124
4140
|
);
|
|
4125
4141
|
}
|
|
4142
|
+
const hasThinking = msg.content.some(item => item.type === "thinking" && item.thinking.length > 0);
|
|
4143
|
+
if (hasThinking) continue;
|
|
4144
|
+
const warningKey = `${msg.api}\0${msg.provider}\0${msg.model}\0${msg.timestamp}`;
|
|
4145
|
+
if (warnedCursorKimiK3ReplayMessages.has(warningKey)) continue;
|
|
4146
|
+
missingThinkingTurns.push(assistantTurn);
|
|
4147
|
+
newlyWarnedKeys.push(warningKey);
|
|
4126
4148
|
}
|
|
4149
|
+
if (missingThinkingTurns.length === 0) return;
|
|
4150
|
+
for (const key of newlyWarnedKeys) warnedCursorKimiK3ReplayMessages.add(key);
|
|
4151
|
+
logger.warn(
|
|
4152
|
+
`Cursor kimi-k3 history contains same-model assistant turn(s) ${missingThinkingTurns.join(", ")} without thinking blocks; replaying those spans without reasoning may make generation less stable`,
|
|
4153
|
+
{ model: targetModelId, assistantTurns: missingThinkingTurns },
|
|
4154
|
+
);
|
|
4127
4155
|
}
|
|
4128
4156
|
|
|
4129
4157
|
/**
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { scheduler } from "node:timers/promises";
|
|
8
|
+
import { type } from "@oh-my-pi/omptype";
|
|
8
9
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
9
10
|
import {
|
|
10
11
|
ANTIGRAVITY_SYSTEM_INSTRUCTION,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
getGeminiCliHeaders,
|
|
14
15
|
} from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
15
16
|
import { extractHttpStatusFromError, fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
|
|
16
|
-
import { type } from "arktype";
|
|
17
17
|
import * as AIError from "../error";
|
|
18
18
|
import type {
|
|
19
19
|
Api,
|
package/src/providers/ollama.ts
CHANGED
|
@@ -251,7 +251,7 @@ function convertMessages(model: Model<"ollama-chat">, context: Context): OllamaM
|
|
|
251
251
|
const messages: Message[] = [...systemMessages, ...context.messages];
|
|
252
252
|
const isCloud = model.provider === "ollama-cloud";
|
|
253
253
|
const supportsImages = model.input.includes("image");
|
|
254
|
-
|
|
254
|
+
const converted = transformMessages(messages, model).map((msg, index) => {
|
|
255
255
|
// Real `systemPrompt` entries (always emitted first) stay on Ollama's
|
|
256
256
|
// `system` role. After the static prefix, a developer turn keeps `system`
|
|
257
257
|
// when it's an agent-owned control instruction (empty/unexpected-stop
|
|
@@ -272,6 +272,21 @@ function convertMessages(model: Model<"ollama-chat">, context: Context): OllamaM
|
|
|
272
272
|
}
|
|
273
273
|
return converted;
|
|
274
274
|
});
|
|
275
|
+
// Ollama returns `done_reason: "load"` and generates nothing when a request
|
|
276
|
+
// carries no `user`-role message (e.g. a plan-approval handoff into a fresh
|
|
277
|
+
// session whose only non-system turn is an agent-attributed developer message
|
|
278
|
+
// mapped to `system`). Demote the last non-prefix system turn to `user` so the
|
|
279
|
+
// request can actually produce output; the static system-prompt prefix stays
|
|
280
|
+
// on `system` for prefix caching. (#7465)
|
|
281
|
+
if (!converted.some(m => m.role === "user")) {
|
|
282
|
+
for (let i = converted.length - 1; i >= systemPrompts.length; i--) {
|
|
283
|
+
if (converted[i].role === "system") {
|
|
284
|
+
converted[i].role = "user";
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return converted;
|
|
275
290
|
}
|
|
276
291
|
|
|
277
292
|
function convertTools(tools: Tool[] | undefined): OllamaFunctionTool[] | undefined {
|
|
@@ -403,6 +418,12 @@ function mapDoneReason(doneReason: string | undefined, output: AssistantMessage)
|
|
|
403
418
|
if (doneReason === "tool_calls") {
|
|
404
419
|
return "toolUse";
|
|
405
420
|
}
|
|
421
|
+
if (doneReason === "load") {
|
|
422
|
+
// Ollama emits done_reason:"load" (model loaded, nothing generated) when a
|
|
423
|
+
// request has no user-role turn. Surface it as an error rather than a clean
|
|
424
|
+
// empty stop so it isn't laundered and retried behind a misleading hint. (#7465)
|
|
425
|
+
return "error";
|
|
426
|
+
}
|
|
406
427
|
if (doneReason === undefined && output.content.some(block => block.type === "toolCall")) {
|
|
407
428
|
return "toolUse";
|
|
408
429
|
}
|
|
@@ -411,6 +432,8 @@ function mapDoneReason(doneReason: string | undefined, output: AssistantMessage)
|
|
|
411
432
|
|
|
412
433
|
const EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE =
|
|
413
434
|
"Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.";
|
|
435
|
+
const EMPTY_OLLAMA_LOAD_COMPLETION_MESSAGE =
|
|
436
|
+
"Ollama loaded the model but generated nothing (done_reason: load): the request contained no user-role message.";
|
|
414
437
|
|
|
415
438
|
function hasVisibleAssistantContent(output: AssistantMessage): boolean {
|
|
416
439
|
return output.content.some(block => {
|
|
@@ -697,6 +720,9 @@ const streamOllamaOnce = (
|
|
|
697
720
|
output.stopReason = "error";
|
|
698
721
|
output.errorMessage = EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE;
|
|
699
722
|
}
|
|
723
|
+
if (output.stopReason === "error" && !output.errorMessage) {
|
|
724
|
+
output.errorMessage = EMPTY_OLLAMA_LOAD_COMPLETION_MESSAGE;
|
|
725
|
+
}
|
|
700
726
|
// Tool calls always mean "execute and continue" in the OpenAI/Ollama contract.
|
|
701
727
|
// If the turn produced tool-call blocks but reported a natural `stop`, promote
|
|
702
728
|
// to `toolUse` so the agent loop runs them (it gates execution on the stop
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import { scheduler } from "node:timers/promises";
|
|
3
|
+
import { type } from "@oh-my-pi/omptype";
|
|
3
4
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
4
5
|
import {
|
|
5
6
|
CODEX_BASE_URL,
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
readSseJson,
|
|
20
21
|
structuredCloneJSON,
|
|
21
22
|
} from "@oh-my-pi/pi-utils";
|
|
22
|
-
import { type } from "arktype";
|
|
23
23
|
import packageJson from "../../package.json" with { type: "json" };
|
|
24
24
|
import * as AIError from "../error";
|
|
25
25
|
import { getEnvApiKey, isOfficialCodexApiUrl } from "../stream";
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Inverse direction (source-of-truth for item shapes): ../../providers/openai-responses.ts
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { type } from "@oh-my-pi/omptype";
|
|
12
13
|
import { logger, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
13
|
-
import { type } from "arktype";
|
|
14
14
|
import { resolvePromptCacheKey } from "../auth-gateway/http";
|
|
15
15
|
import type { AuthGatewayStreamControl, AuthGatewayParsedRequest as ParsedRequest } from "../auth-gateway/types";
|
|
16
16
|
import * as AIError from "../error";
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type AwsBedrockProviderOptions, resolveAwsRegistryApiKey } from "./aws";
|
|
2
2
|
import type { ProviderDefinition } from "./types";
|
|
3
3
|
|
|
4
4
|
export const amazonBedrockProvider = {
|
|
5
5
|
id: "amazon-bedrock",
|
|
6
6
|
name: "Amazon Bedrock",
|
|
7
7
|
// Amazon Bedrock accepts bearer tokens, IAM keys, profiles, ECS/IRSA credential chains.
|
|
8
|
-
envKeys:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
hasEcsCredentials ||
|
|
17
|
-
hasWebIdentity
|
|
18
|
-
) {
|
|
19
|
-
return "<authenticated>";
|
|
20
|
-
}
|
|
8
|
+
envKeys: resolveAwsRegistryApiKey,
|
|
9
|
+
mapSimpleOptions: options => {
|
|
10
|
+
const awsOptions = options.providerOptions as AwsBedrockProviderOptions | undefined;
|
|
11
|
+
return {
|
|
12
|
+
region: awsOptions?.region,
|
|
13
|
+
profile: awsOptions?.profile,
|
|
14
|
+
bearerToken: awsOptions?.bearerToken,
|
|
15
|
+
};
|
|
21
16
|
},
|
|
22
17
|
} as const satisfies ProviderDefinition;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { hasConfiguredAwsProfile } from "../utils/aws-profile";
|
|
4
|
+
import { AUTHENTICATED_SENTINEL } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface AwsBedrockProviderOptions extends Readonly<Record<string, unknown>> {
|
|
7
|
+
/** AWS region used in the service endpoint and SigV4 credential scope. */
|
|
8
|
+
region?: string;
|
|
9
|
+
/** Named AWS shared-credentials/config profile. */
|
|
10
|
+
profile?: string;
|
|
11
|
+
/** Amazon Bedrock API key sent as a bearer token, ahead of SigV4 credential resolution. */
|
|
12
|
+
bearerToken?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isEc2Host(): boolean {
|
|
16
|
+
for (const candidate of [
|
|
17
|
+
"/sys/hypervisor/uuid",
|
|
18
|
+
"/sys/devices/virtual/dmi/id/product_uuid",
|
|
19
|
+
"/sys/devices/virtual/dmi/id/board_asset_tag",
|
|
20
|
+
]) {
|
|
21
|
+
try {
|
|
22
|
+
const value = fs.readFileSync(candidate, "utf8").trim().toLowerCase();
|
|
23
|
+
if (value.startsWith("ec2")) return true;
|
|
24
|
+
} catch {
|
|
25
|
+
// Missing/unreadable DMI metadata means this probe is inconclusive.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hasAwsCredentialSource(): boolean {
|
|
32
|
+
const hasEcsCredentials = !!$env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || !!$env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
|
|
33
|
+
const hasWebIdentity = !!$env.AWS_WEB_IDENTITY_TOKEN_FILE && !!$env.AWS_ROLE_ARN;
|
|
34
|
+
const hasProfile = hasConfiguredAwsProfile();
|
|
35
|
+
const hasInstanceRole =
|
|
36
|
+
$env.AWS_EC2_METADATA_DISABLED?.toLowerCase() !== "true" &&
|
|
37
|
+
(!!$env.AWS_EC2_METADATA_SERVICE_ENDPOINT || isEc2Host());
|
|
38
|
+
return !!(
|
|
39
|
+
($env.AWS_ACCESS_KEY_ID && $env.AWS_SECRET_ACCESS_KEY) ||
|
|
40
|
+
$env.AWS_BEARER_TOKEN_BEDROCK ||
|
|
41
|
+
hasWebIdentity ||
|
|
42
|
+
hasProfile ||
|
|
43
|
+
hasEcsCredentials ||
|
|
44
|
+
hasInstanceRole
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Registry key marker for AWS transports that resolve their own bearer/IAM credentials. */
|
|
49
|
+
export function resolveAwsRegistryApiKey(): string | undefined {
|
|
50
|
+
return hasAwsCredentialSource() ? AUTHENTICATED_SENTINEL : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resolve a real AWS bearer token while filtering the registry's auth marker. */
|
|
54
|
+
export function resolveAwsBearerToken(apiKey?: string, bearerToken?: string): string | undefined {
|
|
55
|
+
const resolvedApiKey = apiKey === AUTHENTICATED_SENTINEL ? undefined : apiKey;
|
|
56
|
+
return bearerToken || resolvedApiKey || $env.AWS_BEARER_TOKEN_BEDROCK;
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BedrockMantleOptions,
|
|
3
|
+
createBedrockMantleAuthenticatedFetch,
|
|
4
|
+
prepareBedrockMantleRequest,
|
|
5
|
+
} from "../providers/bedrock-mantle";
|
|
6
|
+
import type { Model } from "../types";
|
|
7
|
+
import { resolveAwsRegion } from "../utils/aws-profile";
|
|
8
|
+
import { resolveAwsBearerToken, resolveAwsRegistryApiKey } from "./aws";
|
|
9
|
+
import type { ProviderDefinition } from "./types";
|
|
10
|
+
|
|
11
|
+
export const bedrockMantleProvider = {
|
|
12
|
+
id: "bedrock-mantle",
|
|
13
|
+
name: "Amazon Bedrock Mantle",
|
|
14
|
+
envKeys: resolveAwsRegistryApiKey,
|
|
15
|
+
allowsMissingApiKey: true,
|
|
16
|
+
prepareRequest: (model, options) =>
|
|
17
|
+
prepareBedrockMantleRequest(model as Model<"openai-responses">, options as BedrockMantleOptions),
|
|
18
|
+
mapSimpleOptions: options => ({ providerOptions: options.providerOptions }),
|
|
19
|
+
prepareModelDiscovery: config => {
|
|
20
|
+
const bearerToken = resolveAwsBearerToken(config.apiKey);
|
|
21
|
+
if (!bearerToken) {
|
|
22
|
+
return { ...config, apiKey: undefined, authenticated: false };
|
|
23
|
+
}
|
|
24
|
+
const region = resolveAwsRegion();
|
|
25
|
+
return {
|
|
26
|
+
authenticated: true,
|
|
27
|
+
baseUrl: `https://bedrock-mantle.${encodeURIComponent(region)}.api.aws/openai/v1`,
|
|
28
|
+
fetch: createBedrockMantleAuthenticatedFetch({
|
|
29
|
+
fetch: config.fetch,
|
|
30
|
+
providerOptions: { bearerToken, region },
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
} as const satisfies ProviderDefinition;
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import type
|
|
5
|
+
import { AUTHENTICATED_SENTINEL, type ProviderDefinition } from "./types";
|
|
6
6
|
|
|
7
7
|
let cachedVertexAdcCredentialsExists: boolean | null = null;
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ export const googleVertexProvider = {
|
|
|
32
32
|
const hasProject = !!($env.GOOGLE_CLOUD_PROJECT || $env.GCP_PROJECT || $env.GCLOUD_PROJECT);
|
|
33
33
|
const hasLocation = !!($env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION);
|
|
34
34
|
if (hasCredentials && hasProject && hasLocation) {
|
|
35
|
-
return
|
|
35
|
+
return AUTHENTICATED_SENTINEL;
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
} as const satisfies ProviderDefinition;
|
package/src/registry/registry.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { amazonBedrockProvider } from "./amazon-bedrock";
|
|
|
7
7
|
import { anthropicProvider } from "./anthropic";
|
|
8
8
|
import { azureProvider } from "./azure";
|
|
9
9
|
import { basetenProvider } from "./baseten";
|
|
10
|
+
import { bedrockMantleProvider } from "./bedrock-mantle";
|
|
10
11
|
import { cerebrasProvider } from "./cerebras";
|
|
11
12
|
import { cloudflareAiGatewayProvider } from "./cloudflare-ai-gateway";
|
|
12
13
|
import { coreWeaveProvider } from "./coreweave";
|
|
@@ -157,6 +158,7 @@ const ALL = [
|
|
|
157
158
|
mistralProvider,
|
|
158
159
|
minimaxProvider,
|
|
159
160
|
amazonBedrockProvider,
|
|
161
|
+
bedrockMantleProvider,
|
|
160
162
|
gmiCloudProvider,
|
|
161
163
|
];
|
|
162
164
|
|
package/src/registry/types.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* (default model, model-manager factory, catalog discovery) lives in
|
|
10
10
|
* `@oh-my-pi/pi-catalog`'s descriptor table.
|
|
11
11
|
*/
|
|
12
|
+
|
|
13
|
+
import type { Api, FetchImpl, Model, SimpleStreamOptions, StreamOptions } from "../types";
|
|
12
14
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -18,6 +20,26 @@ import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
|
|
|
18
20
|
*/
|
|
19
21
|
export type KeyResolver = string | (() => string | undefined);
|
|
20
22
|
|
|
23
|
+
/** Credentials are resolved by the provider transport rather than used as a bearer string. */
|
|
24
|
+
export const AUTHENTICATED_SENTINEL = "<authenticated>";
|
|
25
|
+
|
|
26
|
+
export interface PreparedProviderRequest {
|
|
27
|
+
readonly model: Model<Api>;
|
|
28
|
+
readonly options: StreamOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ProviderRequestPreparer = (model: Model<Api>, options: StreamOptions) => PreparedProviderRequest;
|
|
32
|
+
export type ProviderSimpleOptionsMapper = (options: SimpleStreamOptions) => Readonly<Record<string, unknown>>;
|
|
33
|
+
|
|
34
|
+
export interface ProviderModelDiscoveryConfig {
|
|
35
|
+
readonly apiKey?: string;
|
|
36
|
+
readonly baseUrl?: string;
|
|
37
|
+
readonly fetch?: FetchImpl;
|
|
38
|
+
readonly authenticated?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ProviderModelDiscoveryPreparer = (config: ProviderModelDiscoveryConfig) => ProviderModelDiscoveryConfig;
|
|
42
|
+
|
|
21
43
|
/**
|
|
22
44
|
* Declarative description of a single provider's auth/login wiring. All
|
|
23
45
|
* fields are optional except `id`/`name`; presence of a field opts the
|
|
@@ -42,6 +64,14 @@ export interface ProviderDefinition {
|
|
|
42
64
|
readonly showInLoginList?: boolean;
|
|
43
65
|
// --- env-var fallback (the catalog table's `envVars` supplies plain names; set this only for computed resolvers) ---
|
|
44
66
|
readonly envKeys?: KeyResolver;
|
|
67
|
+
/** Provider transport can authenticate without a resolved API-key string. */
|
|
68
|
+
readonly allowsMissingApiKey?: boolean;
|
|
69
|
+
/** Provider-owned request shaping applied before generic API dispatch. */
|
|
70
|
+
readonly prepareRequest?: ProviderRequestPreparer;
|
|
71
|
+
/** Provider-owned projection from the generic simple-stream option bag. */
|
|
72
|
+
readonly mapSimpleOptions?: ProviderSimpleOptionsMapper;
|
|
73
|
+
/** Provider-owned authentication and endpoint setup for model discovery. */
|
|
74
|
+
readonly prepareModelDiscovery?: ProviderModelDiscoveryPreparer;
|
|
45
75
|
// --- interactive login (OAuthProviderInterface-compatible) ---
|
|
46
76
|
readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
|
|
47
77
|
readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
package/src/stream.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
streamOpenAIResponses,
|
|
61
61
|
} from "./providers/register-builtins";
|
|
62
62
|
import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
|
|
63
|
-
import { PROVIDER_REGISTRY } from "./registry";
|
|
63
|
+
import { getProviderDefinition, PROVIDER_REGISTRY } from "./registry";
|
|
64
64
|
import type {
|
|
65
65
|
Api,
|
|
66
66
|
AssistantMessage,
|
|
@@ -812,33 +812,37 @@ function streamDispatch<TApi extends Api>(
|
|
|
812
812
|
} as GitLabDuoWorkflowOptions);
|
|
813
813
|
}
|
|
814
814
|
|
|
815
|
-
// Vertex AI
|
|
815
|
+
// Vertex AI and Bedrock Converse authenticate outside the generic API-key path.
|
|
816
816
|
if (model.api === "google-vertex") {
|
|
817
817
|
return streamGoogleVertex(model as Model<"google-vertex">, context, requestOptions as GoogleVertexOptions);
|
|
818
|
-
}
|
|
819
|
-
|
|
818
|
+
}
|
|
819
|
+
if (model.api === "bedrock-converse-stream") {
|
|
820
820
|
return streamBedrock(model as Model<"bedrock-converse-stream">, context, requestOptions as BedrockOptions);
|
|
821
821
|
}
|
|
822
822
|
|
|
823
|
-
const
|
|
823
|
+
const prepareRequest = getProviderDefinition(model.provider)?.prepareRequest;
|
|
824
|
+
const prepared = prepareRequest?.(model as Model<Api>, requestOptions as StreamOptions);
|
|
825
|
+
const providerModel = prepared?.model ?? (model as Model<Api>);
|
|
826
|
+
const preparedOptions = prepared?.options ?? (requestOptions as StreamOptions);
|
|
827
|
+
const apiKey = preparedOptions.apiKey || getEnvApiKey(providerModel.provider);
|
|
824
828
|
if (!apiKey) {
|
|
825
|
-
throw new AIError.MissingApiKeyError(
|
|
829
|
+
throw new AIError.MissingApiKeyError(providerModel.provider);
|
|
826
830
|
}
|
|
827
|
-
const providerOptions = isGoogleVertexAuthenticatedModel(
|
|
831
|
+
const providerOptions = isGoogleVertexAuthenticatedModel(providerModel)
|
|
828
832
|
? {
|
|
829
|
-
...
|
|
833
|
+
...preparedOptions,
|
|
830
834
|
apiKey: "vertex-adc",
|
|
831
|
-
fetch: createVertexAuthenticatedFetch(
|
|
835
|
+
fetch: createVertexAuthenticatedFetch(preparedOptions),
|
|
832
836
|
}
|
|
833
|
-
: { ...
|
|
837
|
+
: { ...preparedOptions, apiKey };
|
|
834
838
|
|
|
835
|
-
const api: Api =
|
|
839
|
+
const api: Api = providerModel.api;
|
|
836
840
|
switch (api) {
|
|
837
841
|
case "anthropic-messages": {
|
|
838
842
|
const anthropicOptions = providerOptions as AnthropicOptions;
|
|
839
|
-
return streamAnthropic(
|
|
843
|
+
return streamAnthropic(providerModel as Model<"anthropic-messages">, context, {
|
|
840
844
|
...anthropicOptions,
|
|
841
|
-
isOAuth: anthropicOptions.isOAuth ??
|
|
845
|
+
isOAuth: anthropicOptions.isOAuth ?? providerModel.isOAuth,
|
|
842
846
|
});
|
|
843
847
|
}
|
|
844
848
|
|
|
@@ -846,13 +850,13 @@ function streamDispatch<TApi extends Api>(
|
|
|
846
850
|
const useResponses = $env.PI_OPENROUTER_RESPONSES !== "0";
|
|
847
851
|
if (useResponses) {
|
|
848
852
|
return streamOpenAIResponses(
|
|
849
|
-
|
|
853
|
+
providerModel as Model<"openai-responses">,
|
|
850
854
|
context,
|
|
851
855
|
providerOptions as OptionsForApi<"openai-responses">,
|
|
852
856
|
);
|
|
853
857
|
}
|
|
854
858
|
return streamOpenAICompletions(
|
|
855
|
-
|
|
859
|
+
providerModel as Model<"openai-completions">,
|
|
856
860
|
context,
|
|
857
861
|
providerOptions as OptionsForApi<"openai-completions">,
|
|
858
862
|
);
|
|
@@ -860,50 +864,50 @@ function streamDispatch<TApi extends Api>(
|
|
|
860
864
|
|
|
861
865
|
case "openai-completions":
|
|
862
866
|
return streamOpenAICompletions(
|
|
863
|
-
|
|
867
|
+
providerModel as Model<"openai-completions">,
|
|
864
868
|
context,
|
|
865
869
|
providerOptions as OptionsForApi<"openai-completions">,
|
|
866
870
|
);
|
|
867
871
|
|
|
868
872
|
case "openai-responses":
|
|
869
873
|
return streamOpenAIResponses(
|
|
870
|
-
|
|
874
|
+
providerModel as Model<"openai-responses">,
|
|
871
875
|
context,
|
|
872
876
|
providerOptions as OptionsForApi<"openai-responses">,
|
|
873
877
|
);
|
|
874
878
|
|
|
875
879
|
case "azure-openai-responses":
|
|
876
880
|
return streamAzureOpenAIResponses(
|
|
877
|
-
|
|
881
|
+
providerModel as Model<"azure-openai-responses">,
|
|
878
882
|
context,
|
|
879
883
|
providerOptions as OptionsForApi<"azure-openai-responses">,
|
|
880
884
|
);
|
|
881
885
|
|
|
882
886
|
case "openai-codex-responses":
|
|
883
887
|
return streamOpenAICodexResponses(
|
|
884
|
-
|
|
888
|
+
providerModel as Model<"openai-codex-responses">,
|
|
885
889
|
context,
|
|
886
890
|
providerOptions as OptionsForApi<"openai-codex-responses">,
|
|
887
891
|
);
|
|
888
892
|
|
|
889
893
|
case "google-generative-ai":
|
|
890
|
-
return streamGoogle(
|
|
894
|
+
return streamGoogle(providerModel as Model<"google-generative-ai">, context, providerOptions);
|
|
891
895
|
|
|
892
896
|
case "google-gemini-cli":
|
|
893
897
|
return streamGoogleGeminiCli(
|
|
894
|
-
|
|
898
|
+
providerModel as Model<"google-gemini-cli">,
|
|
895
899
|
context,
|
|
896
900
|
providerOptions as GoogleGeminiCliOptions,
|
|
897
901
|
);
|
|
898
902
|
|
|
899
903
|
case "ollama-chat":
|
|
900
|
-
return streamOllama(
|
|
904
|
+
return streamOllama(providerModel as Model<"ollama-chat">, context, providerOptions as OllamaChatOptions);
|
|
901
905
|
|
|
902
906
|
case "cursor-agent":
|
|
903
|
-
return streamCursor(
|
|
907
|
+
return streamCursor(providerModel as Model<"cursor-agent">, context, providerOptions as CursorOptions);
|
|
904
908
|
|
|
905
909
|
case "devin-agent":
|
|
906
|
-
return streamDevin(
|
|
910
|
+
return streamDevin(providerModel as Model<"devin-agent">, context, providerOptions as DevinOptions);
|
|
907
911
|
|
|
908
912
|
default:
|
|
909
913
|
throw new AIError.ConfigurationError(`Unhandled API: ${api}`);
|
|
@@ -1028,12 +1032,10 @@ export function streamSimple<TApi extends Api>(
|
|
|
1028
1032
|
if (apiKeyResolver) {
|
|
1029
1033
|
const outer = new AssistantMessageEventStream();
|
|
1030
1034
|
const signal = requestOptions?.signal;
|
|
1031
|
-
// One inner attempt against a resolved
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
|
|
1035
|
-
// emitted directly.
|
|
1036
|
-
const runAttempt = async (apiKey: string): Promise<AuthRetryFailure | undefined> => {
|
|
1035
|
+
// One inner attempt against a resolved key, or against the Bedrock AWS
|
|
1036
|
+
// credential chain when its optional resolver has no stored bearer key.
|
|
1037
|
+
// Retryable auth failures are buffered until replay is safe.
|
|
1038
|
+
const runAttempt = async (apiKey?: string): Promise<AuthRetryFailure | undefined> => {
|
|
1037
1039
|
const bufferedEvents: AssistantMessageEvent[] = [];
|
|
1038
1040
|
let emittedReplayUnsafeEvent = false;
|
|
1039
1041
|
const flushBuffered = (): void => {
|
|
@@ -1107,6 +1109,11 @@ export function streamSimple<TApi extends Api>(
|
|
|
1107
1109
|
return;
|
|
1108
1110
|
}
|
|
1109
1111
|
if (lastKey === undefined) {
|
|
1112
|
+
if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
|
|
1113
|
+
const failure = await runAttempt();
|
|
1114
|
+
if (failure) emitFailure(failure);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1110
1117
|
outer.fail(new AIError.MissingApiKeyError(model.provider));
|
|
1111
1118
|
return;
|
|
1112
1119
|
}
|
|
@@ -1156,6 +1163,13 @@ export function streamSimple<TApi extends Api>(
|
|
|
1156
1163
|
// Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
|
|
1157
1164
|
const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
|
|
1158
1165
|
return stream(model, context, providerOptions);
|
|
1166
|
+
} else if (getProviderDefinition(model.provider)?.allowsMissingApiKey) {
|
|
1167
|
+
const providerOptions = mapOptionsForApi(
|
|
1168
|
+
model,
|
|
1169
|
+
requestOptions,
|
|
1170
|
+
typeof requestOptions.apiKey === "string" ? requestOptions.apiKey : getEnvApiKey(model.provider),
|
|
1171
|
+
);
|
|
1172
|
+
return stream(model, context, providerOptions);
|
|
1159
1173
|
}
|
|
1160
1174
|
|
|
1161
1175
|
// The resolver form is handled by the wrapper above; only a static string
|
|
@@ -1454,6 +1468,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1454
1468
|
apiKey?: string,
|
|
1455
1469
|
): OptionsForApi<TApi> {
|
|
1456
1470
|
const options = normalizeMandatoryReasoningOptions(model, rawOptions);
|
|
1471
|
+
const simpleProviderOptions = getProviderDefinition(model.provider)?.mapSimpleOptions?.(options ?? {});
|
|
1457
1472
|
const base = {
|
|
1458
1473
|
temperature: options?.temperature,
|
|
1459
1474
|
topP: options?.topP,
|
|
@@ -1483,6 +1498,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1483
1498
|
execHandlers: options?.execHandlers,
|
|
1484
1499
|
fetch: options?.fetch,
|
|
1485
1500
|
fallbacks: options?.fallbacks,
|
|
1501
|
+
...simpleProviderOptions,
|
|
1486
1502
|
};
|
|
1487
1503
|
|
|
1488
1504
|
switch (model.api) {
|