@smithers-orchestrator/agents 0.20.0 → 0.20.3
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 -4
- package/src/BaseCliAgent/NormalizedTokenUsage.ts +8 -0
- package/src/BaseCliAgent/extractTextFromJsonValue.js +15 -27
- package/src/BaseCliAgent/index.js +2 -0
- package/src/BaseCliAgent/normalizeTokenUsage.js +90 -0
- package/src/OpenAIAgent.js +19 -5
- package/src/OpenAIAgentOptions.ts +34 -1
- package/src/index.d.ts +27 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/agents",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
4
4
|
"description": "AI SDK and CLI agent adapters for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"ai": "^6.0.168",
|
|
51
51
|
"effect": "^3.21.1",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@smithers-orchestrator/driver": "0.20.
|
|
54
|
-
"@smithers-orchestrator/errors": "0.20.
|
|
55
|
-
"@smithers-orchestrator/observability": "0.20.
|
|
53
|
+
"@smithers-orchestrator/driver": "0.20.3",
|
|
54
|
+
"@smithers-orchestrator/errors": "0.20.3",
|
|
55
|
+
"@smithers-orchestrator/observability": "0.20.3"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/bun": "latest",
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
export function extractTextFromJsonValue(value) {
|
|
6
6
|
if (typeof value === "string")
|
|
7
7
|
return value;
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
const text = value.map((item) => extractTextFromJsonValue(item) ?? "").join("");
|
|
10
|
+
return text || undefined;
|
|
11
|
+
}
|
|
8
12
|
if (!value || typeof value !== "object")
|
|
9
13
|
return undefined;
|
|
10
14
|
const record = /** @type {Record<string, unknown>} */ (value);
|
|
@@ -12,37 +16,21 @@ export function extractTextFromJsonValue(value) {
|
|
|
12
16
|
return record.text;
|
|
13
17
|
if (typeof record.content === "string")
|
|
14
18
|
return record.content;
|
|
19
|
+
if (typeof record.output_text === "string")
|
|
20
|
+
return record.output_text;
|
|
15
21
|
if (Array.isArray(record.content)) {
|
|
16
|
-
const
|
|
17
|
-
.map((part) =>
|
|
18
|
-
if (!part)
|
|
19
|
-
return "";
|
|
20
|
-
if (typeof part === "string")
|
|
21
|
-
return part;
|
|
22
|
-
if (typeof part !== "object")
|
|
23
|
-
return "";
|
|
24
|
-
const partRecord = /** @type {Record<string, unknown>} */ (part);
|
|
25
|
-
if (typeof partRecord.text === "string")
|
|
26
|
-
return partRecord.text;
|
|
27
|
-
if (typeof partRecord.content === "string")
|
|
28
|
-
return partRecord.content;
|
|
29
|
-
return "";
|
|
30
|
-
})
|
|
22
|
+
const text = record.content
|
|
23
|
+
.map((part) => extractTextFromJsonValue(part) ?? "")
|
|
31
24
|
.join("");
|
|
32
|
-
if (
|
|
33
|
-
return
|
|
25
|
+
if (text)
|
|
26
|
+
return text;
|
|
34
27
|
}
|
|
35
28
|
if (record.type === "text" && record.part)
|
|
36
29
|
return extractTextFromJsonValue(record.part);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return extractTextFromJsonValue(record.result);
|
|
43
|
-
if (record.output)
|
|
44
|
-
return extractTextFromJsonValue(record.output);
|
|
45
|
-
if (record.data)
|
|
46
|
-
return extractTextFromJsonValue(record.data);
|
|
30
|
+
for (const field of ["response", "message", "result", "output", "data", "item"]) {
|
|
31
|
+
const text = extractTextFromJsonValue(record[field]);
|
|
32
|
+
if (text)
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
47
35
|
return undefined;
|
|
48
36
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
/** @typedef {import("./BaseCliAgentOptions.ts").BaseCliAgentOptions} BaseCliAgentOptions */
|
|
11
11
|
/** @typedef {import("./CliOutputInterpreter.ts").CliOutputInterpreter} CliOutputInterpreter */
|
|
12
12
|
/** @typedef {import("./CliUsageInfo.ts").CliUsageInfo} CliUsageInfo */
|
|
13
|
+
/** @typedef {import("./NormalizedTokenUsage.ts").NormalizedTokenUsage} NormalizedTokenUsage */
|
|
13
14
|
/** @typedef {import("./CodexConfigOverrides.ts").CodexConfigOverrides} CodexConfigOverrides */
|
|
14
15
|
/** @typedef {import("./PiExtensionUiRequest.ts").PiExtensionUiRequest} PiExtensionUiRequest */
|
|
15
16
|
/** @typedef {import("./PiExtensionUiResponse.ts").PiExtensionUiResponse} PiExtensionUiResponse */
|
|
@@ -21,6 +22,7 @@ export { combineNonEmpty } from "./combineNonEmpty.js";
|
|
|
21
22
|
export { extractPrompt } from "./extractPrompt.js";
|
|
22
23
|
export { tryParseJson } from "./tryParseJson.js";
|
|
23
24
|
export { extractTextFromJsonValue } from "./extractTextFromJsonValue.js";
|
|
25
|
+
export { normalizeTokenUsage } from "./normalizeTokenUsage.js";
|
|
24
26
|
export { createAgentStdoutTextEmitter } from "./createAgentStdoutTextEmitter.js";
|
|
25
27
|
export { truncateToBytes } from "./truncateToBytes.js";
|
|
26
28
|
export { buildGenerateResult } from "./buildGenerateResult.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./NormalizedTokenUsage.ts').NormalizedTokenUsage} NormalizedTokenUsage
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @type {Record<keyof NormalizedTokenUsage, ReadonlyArray<ReadonlyArray<string>>>} */
|
|
6
|
+
const usageFieldAliases = {
|
|
7
|
+
inputTokens: [
|
|
8
|
+
["inputTokens"],
|
|
9
|
+
["promptTokens"],
|
|
10
|
+
["prompt_tokens"],
|
|
11
|
+
["input_tokens"],
|
|
12
|
+
["input"],
|
|
13
|
+
["models", "gemini", "tokens", "input"],
|
|
14
|
+
],
|
|
15
|
+
outputTokens: [
|
|
16
|
+
["outputTokens"],
|
|
17
|
+
["completionTokens"],
|
|
18
|
+
["completion_tokens"],
|
|
19
|
+
["output_tokens"],
|
|
20
|
+
["output"],
|
|
21
|
+
["models", "gemini", "tokens", "output"],
|
|
22
|
+
],
|
|
23
|
+
cacheReadTokens: [
|
|
24
|
+
["cacheReadTokens"],
|
|
25
|
+
["cache_read_input_tokens"],
|
|
26
|
+
["cached_input_tokens"],
|
|
27
|
+
["cache_read_tokens"],
|
|
28
|
+
["inputTokenDetails", "cacheReadTokens"],
|
|
29
|
+
],
|
|
30
|
+
cacheWriteTokens: [
|
|
31
|
+
["cacheWriteTokens"],
|
|
32
|
+
["cache_write_input_tokens"],
|
|
33
|
+
["cache_creation_input_tokens"],
|
|
34
|
+
["cache_write_tokens"],
|
|
35
|
+
["inputTokenDetails", "cacheWriteTokens"],
|
|
36
|
+
],
|
|
37
|
+
reasoningTokens: [
|
|
38
|
+
["reasoningTokens"],
|
|
39
|
+
["reasoning_tokens"],
|
|
40
|
+
["outputTokenDetails", "reasoningTokens"],
|
|
41
|
+
],
|
|
42
|
+
totalTokens: [
|
|
43
|
+
["totalTokens"],
|
|
44
|
+
["total_tokens"],
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
* @param {ReadonlyArray<string>} path
|
|
51
|
+
* @returns {unknown}
|
|
52
|
+
*/
|
|
53
|
+
function readUsagePath(value, path) {
|
|
54
|
+
let current = value;
|
|
55
|
+
for (const segment of path) {
|
|
56
|
+
if (!current || typeof current !== "object")
|
|
57
|
+
return undefined;
|
|
58
|
+
current = /** @type {Record<string, unknown>} */ (current)[segment];
|
|
59
|
+
}
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {NormalizedTokenUsage} usage
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
function hasMeaningfulTokenUsage(usage) {
|
|
68
|
+
return Object.values(usage).some((value) => typeof value === "number" && Number.isFinite(value) && value > 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {unknown} usage
|
|
73
|
+
* @returns {NormalizedTokenUsage | null}
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeTokenUsage(usage) {
|
|
76
|
+
if (!usage || typeof usage !== "object")
|
|
77
|
+
return null;
|
|
78
|
+
/** @type {NormalizedTokenUsage} */
|
|
79
|
+
const normalized = {};
|
|
80
|
+
for (const [field, aliases] of /** @type {Array<[keyof NormalizedTokenUsage, ReadonlyArray<ReadonlyArray<string>>]>} */ (Object.entries(usageFieldAliases))) {
|
|
81
|
+
for (const path of aliases) {
|
|
82
|
+
const value = readUsagePath(usage, path);
|
|
83
|
+
if (typeof value === "number") {
|
|
84
|
+
normalized[field] = value;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return hasMeaningfulTokenUsage(normalized) ? normalized : null;
|
|
90
|
+
}
|
package/src/OpenAIAgent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { openai } from "@ai-sdk/openai";
|
|
1
|
+
import { createOpenAI, openai } from "@ai-sdk/openai";
|
|
2
|
+
import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
2
3
|
import { Output, ToolLoopAgent, } from "ai";
|
|
3
4
|
import { resolveSdkModel } from "./resolveSdkModel.js";
|
|
4
5
|
import { streamResultToGenerateResult } from "./streamResultToGenerateResult.js";
|
|
@@ -17,16 +18,29 @@ import { streamResultToGenerateResult } from "./streamResultToGenerateResult.js"
|
|
|
17
18
|
|
|
18
19
|
export class OpenAIAgent extends ToolLoopAgent {
|
|
19
20
|
hijackEngine = "openai-sdk";
|
|
20
|
-
supportsNativeStructuredOutput = true;
|
|
21
21
|
/**
|
|
22
22
|
* @param {OpenAIAgentOptions<CALL_OPTIONS, TOOLS>} opts
|
|
23
23
|
*/
|
|
24
24
|
constructor(opts) {
|
|
25
|
-
const { model, ...rest } = opts;
|
|
25
|
+
const { model, baseURL, apiKey, nativeStructuredOutput, ...rest } = opts;
|
|
26
|
+
const hasProviderConfig = baseURL !== undefined || apiKey !== undefined;
|
|
27
|
+
if (hasProviderConfig && typeof model !== "string") {
|
|
28
|
+
throw new SmithersError("AGENT_CONFIG_INVALID", "OpenAIAgent baseURL/apiKey can only be used when model is a string. For a prebuilt model, put provider settings in createOpenAI({ baseURL, apiKey }) before calling the provider.", {
|
|
29
|
+
hasBaseURL: baseURL !== undefined,
|
|
30
|
+
hasApiKey: apiKey !== undefined,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const provider = hasProviderConfig
|
|
34
|
+
? createOpenAI({
|
|
35
|
+
...(baseURL !== undefined ? { baseURL } : {}),
|
|
36
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
37
|
+
})
|
|
38
|
+
: openai;
|
|
26
39
|
super({
|
|
27
40
|
...rest,
|
|
28
|
-
model: resolveSdkModel(model,
|
|
41
|
+
model: resolveSdkModel(model, provider),
|
|
29
42
|
});
|
|
43
|
+
this.supportsNativeStructuredOutput = nativeStructuredOutput !== false;
|
|
30
44
|
}
|
|
31
45
|
/**
|
|
32
46
|
* @param {AgentGenerateOptions} [args]
|
|
@@ -36,7 +50,7 @@ export class OpenAIAgent extends ToolLoopAgent {
|
|
|
36
50
|
const promptArgs = "messages" in args
|
|
37
51
|
? { messages: args.messages }
|
|
38
52
|
: { prompt: args.prompt };
|
|
39
|
-
const outputArgs = args.outputSchema
|
|
53
|
+
const outputArgs = this.supportsNativeStructuredOutput && args.outputSchema
|
|
40
54
|
? { output: Output.object({ schema: args.outputSchema }) }
|
|
41
55
|
: {};
|
|
42
56
|
if (!args.onStdout) {
|
|
@@ -2,7 +2,40 @@ import type { openai } from "@ai-sdk/openai";
|
|
|
2
2
|
import type { ToolSet } from "ai";
|
|
3
3
|
import type { SdkAgentOptions } from "./SdkAgentOptions";
|
|
4
4
|
|
|
5
|
+
type OpenAIAgentCommonOptions<
|
|
6
|
+
CALL_OPTIONS,
|
|
7
|
+
TOOLS extends ToolSet,
|
|
8
|
+
> = Omit<
|
|
9
|
+
SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof openai>>,
|
|
10
|
+
"model"
|
|
11
|
+
> & {
|
|
12
|
+
/**
|
|
13
|
+
* Disable AI SDK native structured output and let Smithers use prompt-based JSON extraction.
|
|
14
|
+
* Useful for OpenAI-compatible local servers that do not honor JSON schema response formats.
|
|
15
|
+
*/
|
|
16
|
+
nativeStructuredOutput?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type OpenAIAgentStringModelOptions = {
|
|
20
|
+
model: string;
|
|
21
|
+
/**
|
|
22
|
+
* Base URL for OpenAI-compatible API calls, e.g. a local llama.cpp server.
|
|
23
|
+
*/
|
|
24
|
+
baseURL?: string;
|
|
25
|
+
/**
|
|
26
|
+
* API key sent to OpenAI-compatible endpoints. Local servers often accept "none".
|
|
27
|
+
*/
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type OpenAIAgentPrebuiltModelOptions = {
|
|
32
|
+
model: ReturnType<typeof openai>;
|
|
33
|
+
baseURL?: never;
|
|
34
|
+
apiKey?: never;
|
|
35
|
+
};
|
|
36
|
+
|
|
5
37
|
export type OpenAIAgentOptions<
|
|
6
38
|
CALL_OPTIONS = never,
|
|
7
39
|
TOOLS extends ToolSet = {},
|
|
8
|
-
> =
|
|
40
|
+
> = OpenAIAgentCommonOptions<CALL_OPTIONS, TOOLS> &
|
|
41
|
+
(OpenAIAgentStringModelOptions | OpenAIAgentPrebuiltModelOptions);
|
package/src/index.d.ts
CHANGED
|
@@ -101,7 +101,33 @@ type SdkAgentOptions<CALL_OPTIONS = never, TOOLS extends ToolSet = {}, MODEL = a
|
|
|
101
101
|
model: string | MODEL;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
type
|
|
104
|
+
type OpenAIAgentCommonOptions<CALL_OPTIONS, TOOLS extends ToolSet> = Omit<SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof openai>>, "model"> & {
|
|
105
|
+
/**
|
|
106
|
+
* Disable AI SDK native structured output and let Smithers use prompt-based JSON extraction.
|
|
107
|
+
* Useful for OpenAI-compatible local servers that do not honor JSON schema response formats.
|
|
108
|
+
*/
|
|
109
|
+
nativeStructuredOutput?: boolean;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type OpenAIAgentStringModelOptions = {
|
|
113
|
+
model: string;
|
|
114
|
+
/**
|
|
115
|
+
* Base URL for OpenAI-compatible API calls, e.g. a local llama.cpp server.
|
|
116
|
+
*/
|
|
117
|
+
baseURL?: string;
|
|
118
|
+
/**
|
|
119
|
+
* API key sent to OpenAI-compatible endpoints. Local servers often accept "none".
|
|
120
|
+
*/
|
|
121
|
+
apiKey?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
type OpenAIAgentPrebuiltModelOptions = {
|
|
125
|
+
model: ReturnType<typeof openai>;
|
|
126
|
+
baseURL?: never;
|
|
127
|
+
apiKey?: never;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type OpenAIAgentOptions$2<CALL_OPTIONS = never, TOOLS extends ToolSet = {}> = OpenAIAgentCommonOptions<CALL_OPTIONS, TOOLS> & (OpenAIAgentStringModelOptions | OpenAIAgentPrebuiltModelOptions);
|
|
105
131
|
|
|
106
132
|
type AnthropicAgentOptions$2<CALL_OPTIONS = never, TOOLS extends ToolSet = {}> = SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof anthropic>>;
|
|
107
133
|
|