@capekai/core 1.0.9 → 1.0.11
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 +4 -5
- package/src/adapters/ai-sdk.ts +6 -2
- package/src/adapters/codex-network-retry.ts +25 -0
- package/src/core/agent.ts +2 -2
- package/src/core/message-utils.ts +8 -0
- package/src/core/model-utils.ts +8 -24
- package/src/core/stream/stream-config.ts +5 -1
- package/src/providers/protocol-model.ts +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capekai/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Bun-native composable agent runtime and framework for Capek.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -75,8 +75,9 @@
|
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@ai-sdk/
|
|
78
|
+
"@ai-sdk/anthropic": "^3.0.116",
|
|
79
79
|
"@ai-sdk/openai": "^3.0.84",
|
|
80
|
+
"@ai-sdk/openai-compatible": "^2.0.74",
|
|
80
81
|
"@capekai/tool": "^1.0.3",
|
|
81
82
|
"@capekai/types": "^1.0.3",
|
|
82
83
|
"@openrouter/ai-sdk-provider": "^2.3.3",
|
|
@@ -84,9 +85,7 @@
|
|
|
84
85
|
"ai": "^6.0.116",
|
|
85
86
|
"ignore": "^7.0.3",
|
|
86
87
|
"picomatch": "^4.0.2",
|
|
87
|
-
"tar": "^7.5.13"
|
|
88
|
-
"vercel-minimax-ai-provider": "^0.0.2",
|
|
89
|
-
"zhipu-ai-provider": "^0.2.2"
|
|
88
|
+
"tar": "^7.5.13"
|
|
90
89
|
},
|
|
91
90
|
"scripts": {
|
|
92
91
|
"build": "tsc --noEmit",
|
package/src/adapters/ai-sdk.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
2
|
import {
|
|
3
|
+
wrapLanguageModel,
|
|
3
4
|
dynamicTool,
|
|
4
5
|
jsonSchema,
|
|
5
6
|
streamText,
|
|
6
7
|
type JSONSchema7,
|
|
7
|
-
type LanguageModel,
|
|
8
8
|
type Tool,
|
|
9
9
|
} from 'ai';
|
|
10
10
|
import { getModelWithMetadata } from '../core/model-utils';
|
|
11
11
|
import { openAiModelOmitsTemperature } from '../core/provider-utils';
|
|
12
12
|
import type { ModelFactoryResult } from '../providers/types';
|
|
13
|
+
import { codexNetworkRetryMiddleware } from './codex-network-retry';
|
|
13
14
|
|
|
14
15
|
export interface TextModelRequest {
|
|
15
16
|
modelId?: string;
|
|
@@ -55,7 +56,10 @@ export function createOpenAiResponsesModel(request: OpenAiResponsesModelRequest)
|
|
|
55
56
|
fetch: request.fetch,
|
|
56
57
|
});
|
|
57
58
|
return {
|
|
58
|
-
model:
|
|
59
|
+
model: wrapLanguageModel({
|
|
60
|
+
model: openai.responses(request.modelId),
|
|
61
|
+
middleware: codexNetworkRetryMiddleware,
|
|
62
|
+
}),
|
|
59
63
|
useProviderInstructions: true,
|
|
60
64
|
omitMaxOutputTokens: true,
|
|
61
65
|
omitTemperature: openAiModelOmitsTemperature(request.modelId),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { APICallError, type LanguageModelMiddleware } from 'ai';
|
|
2
|
+
import { ApiErrorType, classifyApiError } from '../utils/errors';
|
|
3
|
+
|
|
4
|
+
export const codexNetworkRetryMiddleware: LanguageModelMiddleware = {
|
|
5
|
+
specificationVersion: 'v3',
|
|
6
|
+
wrapStream: async ({ doStream, params }) => {
|
|
7
|
+
try {
|
|
8
|
+
return await doStream();
|
|
9
|
+
} catch (error: unknown) {
|
|
10
|
+
if (params.abortSignal?.aborted || APICallError.isInstance(error)
|
|
11
|
+
|| (error instanceof Error && error.name === 'AbortError')) throw error;
|
|
12
|
+
const classified = classifyApiError(error);
|
|
13
|
+
if (classified.type !== ApiErrorType.Network) throw error;
|
|
14
|
+
// OpenAI's early stream probe can throw a raw socket error. Normalize
|
|
15
|
+
// only before stream handoff so SDK retries this request, not prior tools.
|
|
16
|
+
throw new APICallError({
|
|
17
|
+
message: classified.message,
|
|
18
|
+
url: 'https://chatgpt.com/backend-api/codex/responses',
|
|
19
|
+
requestBodyValues: undefined,
|
|
20
|
+
cause: error,
|
|
21
|
+
isRetryable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
package/src/core/agent.ts
CHANGED
|
@@ -144,7 +144,7 @@ export async function* streamChat(options: ChatOptions): AsyncGenerator<(Message
|
|
|
144
144
|
selfDelegationAvailable,
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
const { model, useProviderInstructions, omitMaxOutputTokens, omitTemperature, providerOptions: baseProviderOptions } =
|
|
147
|
+
const { model, replayReasoning, useProviderInstructions, omitMaxOutputTokens, omitTemperature, providerOptions: baseProviderOptions } =
|
|
148
148
|
await getModelWithMetadata({
|
|
149
149
|
modelId: resolvedModelId,
|
|
150
150
|
providerId,
|
|
@@ -157,7 +157,7 @@ export async function* streamChat(options: ChatOptions): AsyncGenerator<(Message
|
|
|
157
157
|
|
|
158
158
|
// Convert messages for ai-sdk
|
|
159
159
|
const modelDef = resolvedModelId ? findModel(resolvedModelId) : undefined;
|
|
160
|
-
const aiMessages = await convertToAiSdkMessages(messages, modelDef?.capabilities);
|
|
160
|
+
const aiMessages = await convertToAiSdkMessages(messages, modelDef?.capabilities, { replayReasoning });
|
|
161
161
|
if (options.continueFromCompaction) {
|
|
162
162
|
// Execution instruction only: do not persist it as another user request.
|
|
163
163
|
aiMessages.push({ role: 'user', content: 'Continue the existing task from the checkpoint above. Preserve completed work and tool outcomes; do not restart or repeat completed actions. Follow the remaining steps, or report completion if nothing remains.' });
|
|
@@ -47,6 +47,7 @@ async function resolveAttachmentPath(part: ImagePart | FilePart): Promise<{ abso
|
|
|
47
47
|
export async function convertToAiSdkMessages(
|
|
48
48
|
messages: MessageWithParts[],
|
|
49
49
|
modelCapabilities?: ModelCapabilities,
|
|
50
|
+
options: { replayReasoning?: boolean } = {},
|
|
50
51
|
): Promise<ModelMessage[]> {
|
|
51
52
|
const result: { role: 'user' | 'assistant' | 'system' | 'tool'; content: AiSdkContent }[] = [];
|
|
52
53
|
|
|
@@ -54,6 +55,11 @@ export async function convertToAiSdkMessages(
|
|
|
54
55
|
const msg = msgWithParts.message;
|
|
55
56
|
const parts = msgWithParts.parts;
|
|
56
57
|
|
|
58
|
+
const reasoningOptions = options.replayReasoning && msg.role === 'assistant'
|
|
59
|
+
? { providerOptions: { openaiCompatible: {
|
|
60
|
+
reasoning_content: parts.filter((part) => part.type === 'reasoning').map((part) => part.text).join(''),
|
|
61
|
+
} } }
|
|
62
|
+
: {};
|
|
57
63
|
const textBlocks: string[] = [];
|
|
58
64
|
const toolCallBlocks: Array<{
|
|
59
65
|
type: 'tool-call';
|
|
@@ -243,6 +249,7 @@ export async function convertToAiSdkMessages(
|
|
|
243
249
|
result.push({
|
|
244
250
|
role: hasCompactionTrigger ? 'user' : (msg.role as 'user' | 'assistant' | 'system'),
|
|
245
251
|
content,
|
|
252
|
+
...(!hasCompactionTrigger ? reasoningOptions : {}),
|
|
246
253
|
});
|
|
247
254
|
continue;
|
|
248
255
|
}
|
|
@@ -250,6 +257,7 @@ export async function convertToAiSdkMessages(
|
|
|
250
257
|
result.push({
|
|
251
258
|
role: msg.role as 'user' | 'assistant' | 'system',
|
|
252
259
|
content: contentParts,
|
|
260
|
+
...reasoningOptions,
|
|
253
261
|
});
|
|
254
262
|
|
|
255
263
|
for (const toolResult of toolResultBlocks) {
|
package/src/core/model-utils.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { isSandboxActive } from '../runtime/host-dependencies';
|
|
|
7
7
|
|
|
8
8
|
export interface ModelWithMetadata {
|
|
9
9
|
model: LanguageModel;
|
|
10
|
+
/** Replay stored reasoning only for adapters that accept unsigned reasoning text. */
|
|
11
|
+
replayReasoning?: boolean;
|
|
10
12
|
useProviderInstructions?: boolean;
|
|
11
13
|
omitMaxOutputTokens?: boolean;
|
|
12
14
|
omitTemperature?: boolean;
|
|
@@ -97,34 +99,16 @@ export async function getModelWithMetadata(
|
|
|
97
99
|
return { model: openrouter.chat(model) as unknown as LanguageModel };
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
case 'minimax':
|
|
101
|
-
|
|
102
|
-
const minimax = createMinimax({ apiKey });
|
|
103
|
-
return { model: minimax.chat(model) as unknown as LanguageModel };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
case 'zhipu': {
|
|
107
|
-
const { createZhipu } = await import('zhipu-ai-provider');
|
|
108
|
-
const zhipu = createZhipu({
|
|
109
|
-
apiKey,
|
|
110
|
-
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
|
111
|
-
});
|
|
112
|
-
return { model: zhipu.chat(model) as unknown as LanguageModel };
|
|
113
|
-
}
|
|
114
|
-
|
|
102
|
+
case 'minimax':
|
|
103
|
+
case 'zhipu':
|
|
115
104
|
case 'zhipu-coding': {
|
|
116
|
-
const {
|
|
117
|
-
|
|
118
|
-
apiKey,
|
|
119
|
-
baseURL: 'https://api.z.ai/api/coding/paas/v4',
|
|
120
|
-
});
|
|
121
|
-
return { model: zhipu.chat(model) as unknown as LanguageModel };
|
|
105
|
+
const { createProtocolModel } = await import('../providers/protocol-model');
|
|
106
|
+
return { model: createProtocolModel(provider, model, apiKey) };
|
|
122
107
|
}
|
|
123
108
|
|
|
124
109
|
case 'deepseek': {
|
|
125
|
-
const {
|
|
126
|
-
|
|
127
|
-
return { model: deepseek.chat(model) as unknown as LanguageModel };
|
|
110
|
+
const { createProtocolModel } = await import('../providers/protocol-model');
|
|
111
|
+
return { model: createProtocolModel(provider, model, apiKey), replayReasoning: true };
|
|
128
112
|
}
|
|
129
113
|
|
|
130
114
|
case 'openai':
|
|
@@ -43,7 +43,11 @@ export function buildStreamConfig(options: StreamConfigOptions): StreamConfigRes
|
|
|
43
43
|
// Determine the provider-specific providerOptions key
|
|
44
44
|
const resolvedProvider = providerId || getModelsConfig().defaultProvider;
|
|
45
45
|
const registered = resolvedProvider ? getProvider(resolvedProvider) : undefined;
|
|
46
|
-
|
|
46
|
+
// Built-in MiniMax uses Anthropic's option namespace. Registered overrides
|
|
47
|
+
// retain their own namespace unless their descriptor explicitly changes it.
|
|
48
|
+
const providerOptionsKey = registered
|
|
49
|
+
? registered.descriptor.providerOptionsKey ?? resolvedProvider
|
|
50
|
+
: resolvedProvider === 'minimax' ? 'anthropic' : resolvedProvider;
|
|
47
51
|
|
|
48
52
|
// Build merged providerOptions
|
|
49
53
|
let providerOptions: Record<string, Record<string, unknown>> | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
3
|
+
import { wrapLanguageModel, type LanguageModel } from 'ai';
|
|
4
|
+
|
|
5
|
+
export type ProtocolProvider = 'minimax' | 'zhipu' | 'zhipu-coding' | 'deepseek';
|
|
6
|
+
|
|
7
|
+
export function createProtocolModel(
|
|
8
|
+
providerId: ProtocolProvider,
|
|
9
|
+
modelId: string,
|
|
10
|
+
apiKey: string,
|
|
11
|
+
fetch?: NonNullable<Parameters<typeof createAnthropic>[0]>['fetch'],
|
|
12
|
+
): LanguageModel {
|
|
13
|
+
if (providerId === 'minimax') {
|
|
14
|
+
return createAnthropic({ apiKey, baseURL: 'https://api.minimax.io/anthropic/v1', fetch })(modelId);
|
|
15
|
+
}
|
|
16
|
+
const model = createOpenAICompatible({
|
|
17
|
+
name: providerId,
|
|
18
|
+
apiKey,
|
|
19
|
+
baseURL: providerId === 'deepseek' ? 'https://api.deepseek.com/v1' : providerId === 'zhipu'
|
|
20
|
+
? 'https://open.bigmodel.cn/api/paas/v4'
|
|
21
|
+
: 'https://api.z.ai/api/coding/paas/v4',
|
|
22
|
+
fetch,
|
|
23
|
+
})(modelId);
|
|
24
|
+
if (providerId !== 'deepseek') return model;
|
|
25
|
+
return wrapLanguageModel({
|
|
26
|
+
model,
|
|
27
|
+
middleware: {
|
|
28
|
+
specificationVersion: 'v3',
|
|
29
|
+
transformParams: async ({ params }) => ({
|
|
30
|
+
...params,
|
|
31
|
+
prompt: params.prompt.map((message) => {
|
|
32
|
+
if (message.role !== 'assistant') return message;
|
|
33
|
+
const reasoning = message.content.filter((part) => part.type === 'reasoning');
|
|
34
|
+
const existing = message.providerOptions?.openaiCompatible?.reasoning_content;
|
|
35
|
+
return {
|
|
36
|
+
...message,
|
|
37
|
+
content: message.content.filter((part) => part.type !== 'reasoning'),
|
|
38
|
+
providerOptions: {
|
|
39
|
+
...message.providerOptions,
|
|
40
|
+
openaiCompatible: {
|
|
41
|
+
...message.providerOptions?.openaiCompatible,
|
|
42
|
+
// The SDK omits empty reasoning by default. DeepSeek needs the field
|
|
43
|
+
// on tool continuations too, including SDK-generated assistant steps.
|
|
44
|
+
reasoning_content: reasoning.length > 0
|
|
45
|
+
? reasoning.map((part) => part.text).join('')
|
|
46
|
+
: typeof existing === 'string' ? existing : '',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|