@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.20.0",
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.0",
54
- "@smithers-orchestrator/errors": "0.20.0",
55
- "@smithers-orchestrator/observability": "0.20.0"
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",
@@ -0,0 +1,8 @@
1
+ export type NormalizedTokenUsage = {
2
+ inputTokens?: number;
3
+ outputTokens?: number;
4
+ cacheReadTokens?: number;
5
+ cacheWriteTokens?: number;
6
+ reasoningTokens?: number;
7
+ totalTokens?: number;
8
+ };
@@ -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 parts = record.content
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 (parts.trim())
33
- return parts;
25
+ if (text)
26
+ return text;
34
27
  }
35
28
  if (record.type === "text" && record.part)
36
29
  return extractTextFromJsonValue(record.part);
37
- if (record.response)
38
- return extractTextFromJsonValue(record.response);
39
- if (record.message)
40
- return extractTextFromJsonValue(record.message);
41
- if (record.result)
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
+ }
@@ -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, openai),
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
- > = SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof openai>>;
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 OpenAIAgentOptions$2<CALL_OPTIONS = never, TOOLS extends ToolSet = {}> = SdkAgentOptions<CALL_OPTIONS, TOOLS, ReturnType<typeof openai>>;
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