@volter/twin-openai 2.0.6 → 2.0.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.
|
@@ -57,6 +57,8 @@ export const openaiScenarioAdapter = {
|
|
|
57
57
|
modelEquals: (req, cond) => nonEmptyString(cond) && req.model === cond,
|
|
58
58
|
userTextIncludes: (req, cond) => nonEmptyString(cond) && lastUserText(req.messages).toLowerCase().includes(cond.toLowerCase()),
|
|
59
59
|
anyTextIncludes: (req, cond) => nonEmptyString(cond) && req.messages.map((m) => contentToText(m.content)).join('\n').toLowerCase().includes(cond.toLowerCase()),
|
|
60
|
+
// the latest message alone (a tool's result just returned, not one from earlier in the conversation)
|
|
61
|
+
lastMessageTextIncludes: (req, cond) => nonEmptyString(cond) && contentToText(req.messages[req.messages.length - 1]?.content).toLowerCase().includes(cond.toLowerCase()),
|
|
60
62
|
lastMessageIsToolResult: (req, cond) => typeof cond === 'boolean' && (req.messages[req.messages.length - 1]?.role === 'tool') === cond,
|
|
61
63
|
toolResultFor: (req, cond) => nonEmptyString(cond) && lastToolResultNames(req.messages).has(cond),
|
|
62
64
|
hasTool: (req, cond) => nonEmptyString(cond) && toolNames(req.tools).includes(cond),
|
|
@@ -66,7 +68,7 @@ export const openaiScenarioAdapter = {
|
|
|
66
68
|
},
|
|
67
69
|
text: (req) => req.messages.map((m) => contentToText(m.content)).join('\n'),
|
|
68
70
|
validateOn: (on) => {
|
|
69
|
-
for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'toolResultFor', 'hasTool'])
|
|
71
|
+
for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'lastMessageTextIncludes', 'toolResultFor', 'hasTool'])
|
|
70
72
|
if (on[k] !== undefined && (typeof on[k] !== 'string' || !on[k]))
|
|
71
73
|
return `on.${k} is a non-empty string`;
|
|
72
74
|
if (on.lastMessageIsToolResult !== undefined && typeof on.lastMessageIsToolResult !== 'boolean')
|
|
@@ -20,8 +20,12 @@ export declare function stubAssistantText(messages: ChatMessageParam[], model: s
|
|
|
20
20
|
/**
|
|
21
21
|
* Build a deterministic stub argument string for a tool. When the tool declares a JSON-schema
|
|
22
22
|
* `parameters` object (especially with `strict:true`), real models emit arguments that validate
|
|
23
|
-
* against the schema; the twin synthesizes a deterministic object
|
|
24
|
-
*
|
|
23
|
+
* against the schema; the twin synthesizes a deterministic object with a type-appropriate
|
|
24
|
+
* placeholder for each property it sets, so `strict` callers parse it cleanly. A strict tool, or a
|
|
25
|
+
* schema that names no `required` list, gets every declared property (strict makes every property
|
|
26
|
+
* required); otherwise only the required ones: a model leaves an optional parameter it has no reason
|
|
27
|
+
* to set unset, and a placeholder there is a choice nobody made (LibreChat's web_search took an
|
|
28
|
+
* optional `date` enum's first value, "past hour", and its search was refused). A required
|
|
25
29
|
* string is never left empty: an empty value is schema-valid in type but names nothing, so an
|
|
26
30
|
* application's tool could not act on it and a story could not follow the call. It takes what the
|
|
27
31
|
* user's message names (`namedIn`), else a deterministic value of the parameter's own name; an enum
|
package/dist/src/openai-stub.js
CHANGED
|
@@ -95,8 +95,12 @@ function namedIn(userText) {
|
|
|
95
95
|
/**
|
|
96
96
|
* Build a deterministic stub argument string for a tool. When the tool declares a JSON-schema
|
|
97
97
|
* `parameters` object (especially with `strict:true`), real models emit arguments that validate
|
|
98
|
-
* against the schema; the twin synthesizes a deterministic object
|
|
99
|
-
*
|
|
98
|
+
* against the schema; the twin synthesizes a deterministic object with a type-appropriate
|
|
99
|
+
* placeholder for each property it sets, so `strict` callers parse it cleanly. A strict tool, or a
|
|
100
|
+
* schema that names no `required` list, gets every declared property (strict makes every property
|
|
101
|
+
* required); otherwise only the required ones: a model leaves an optional parameter it has no reason
|
|
102
|
+
* to set unset, and a placeholder there is a choice nobody made (LibreChat's web_search took an
|
|
103
|
+
* optional `date` enum's first value, "past hour", and its search was refused). A required
|
|
100
104
|
* string is never left empty: an empty value is schema-valid in type but names nothing, so an
|
|
101
105
|
* application's tool could not act on it and a story could not follow the call. It takes what the
|
|
102
106
|
* user's message names (`namedIn`), else a deterministic value of the parameter's own name; an enum
|
|
@@ -109,9 +113,13 @@ export function stubToolArguments(tool, userText = '') {
|
|
|
109
113
|
if (!props || typeof props !== 'object')
|
|
110
114
|
return '{}';
|
|
111
115
|
const required = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
|
|
116
|
+
const strict = tool?.function?.strict === true || tool?.strict === true;
|
|
117
|
+
const onlyRequired = !strict && Array.isArray(schema.required);
|
|
112
118
|
const words = new Set(userText.toLowerCase().match(/[a-z0-9-]+/g) ?? []);
|
|
113
119
|
const out = {};
|
|
114
120
|
for (const [key, def] of Object.entries(props)) {
|
|
121
|
+
if (onlyRequired && !required.has(key))
|
|
122
|
+
continue;
|
|
115
123
|
const d = def;
|
|
116
124
|
const named = Array.isArray(d?.enum) ? d.enum.find((e) => typeof e === 'string' && words.has(e.toLowerCase())) : undefined;
|
|
117
125
|
out[key] = named ?? (required.has(key) && d?.type === 'string' && !Array.isArray(d?.enum) ? namedIn(userText) ?? `twin-${key}` : placeholderForSchema(def));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-openai",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "Local OpenAI twin — a faithful, stateful local OpenAI API your real `openai` SDK talks to unmodified. The model is stubbed (deterministic) and embeddings are deterministic pseudo-vectors, but the protocol envelope (chat completions/streaming/tool_calls, the Responses API, embeddings, models, files, batches, moderations, fine-tuning, vector stores) is vendor-faithful. Built on @volter/world-core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@volter/world-core": "2.0.
|
|
57
|
+
"@volter/world-core": "2.0.7"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@volter/world-core": "2.0.
|
|
60
|
+
"@volter/world-core": "2.0.7",
|
|
61
61
|
"@volter/world-tooling": "0.1.0",
|
|
62
62
|
"openai": "^4.104.0",
|
|
63
63
|
"@types/bun": "^1.2.20",
|
|
@@ -68,6 +68,6 @@
|
|
|
68
68
|
"node": ">=22.3"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@volter/world-ui": "0.1.
|
|
71
|
+
"@volter/world-ui": "0.1.7"
|
|
72
72
|
}
|
|
73
73
|
}
|
package/src/openai-scenario.ts
CHANGED
|
@@ -77,6 +77,8 @@ export const openaiScenarioAdapter: PackScenarioAdapter<OpenAIScenarioRequest> =
|
|
|
77
77
|
modelEquals: (req, cond) => nonEmptyString(cond) && req.model === cond,
|
|
78
78
|
userTextIncludes: (req, cond) => nonEmptyString(cond) && lastUserText(req.messages).toLowerCase().includes(cond.toLowerCase()),
|
|
79
79
|
anyTextIncludes: (req, cond) => nonEmptyString(cond) && req.messages.map((m) => contentToText(m.content)).join('\n').toLowerCase().includes(cond.toLowerCase()),
|
|
80
|
+
// the latest message alone (a tool's result just returned, not one from earlier in the conversation)
|
|
81
|
+
lastMessageTextIncludes: (req, cond) => nonEmptyString(cond) && contentToText((req.messages[req.messages.length - 1] as { content?: unknown } | undefined)?.content as never).toLowerCase().includes(cond.toLowerCase()),
|
|
80
82
|
lastMessageIsToolResult: (req, cond) => typeof cond === 'boolean' && ((req.messages[req.messages.length - 1] as { role?: string } | undefined)?.role === 'tool') === cond,
|
|
81
83
|
toolResultFor: (req, cond) => nonEmptyString(cond) && lastToolResultNames(req.messages).has(cond),
|
|
82
84
|
hasTool: (req, cond) => nonEmptyString(cond) && toolNames(req.tools).includes(cond),
|
|
@@ -86,7 +88,7 @@ export const openaiScenarioAdapter: PackScenarioAdapter<OpenAIScenarioRequest> =
|
|
|
86
88
|
},
|
|
87
89
|
text: (req) => req.messages.map((m) => contentToText(m.content)).join('\n'),
|
|
88
90
|
validateOn: (on) => {
|
|
89
|
-
for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'toolResultFor', 'hasTool'] as const) if (on[k] !== undefined && (typeof on[k] !== 'string' || !on[k])) return `on.${k} is a non-empty string`;
|
|
91
|
+
for (const k of ['modelEquals', 'userTextIncludes', 'anyTextIncludes', 'lastMessageTextIncludes', 'toolResultFor', 'hasTool'] as const) if (on[k] !== undefined && (typeof on[k] !== 'string' || !on[k])) return `on.${k} is a non-empty string`;
|
|
90
92
|
if (on.lastMessageIsToolResult !== undefined && typeof on.lastMessageIsToolResult !== 'boolean') return 'on.lastMessageIsToolResult is a boolean';
|
|
91
93
|
for (const k of ['maxTokensBelow', 'maxTokensAtLeast'] as const) if (on[k] !== undefined && !positiveNumber(on[k])) return `on.${k} is a positive number`;
|
|
92
94
|
return null;
|
package/src/openai-stub.ts
CHANGED
|
@@ -100,8 +100,12 @@ function namedIn(userText: string): string | undefined {
|
|
|
100
100
|
/**
|
|
101
101
|
* Build a deterministic stub argument string for a tool. When the tool declares a JSON-schema
|
|
102
102
|
* `parameters` object (especially with `strict:true`), real models emit arguments that validate
|
|
103
|
-
* against the schema; the twin synthesizes a deterministic object
|
|
104
|
-
*
|
|
103
|
+
* against the schema; the twin synthesizes a deterministic object with a type-appropriate
|
|
104
|
+
* placeholder for each property it sets, so `strict` callers parse it cleanly. A strict tool, or a
|
|
105
|
+
* schema that names no `required` list, gets every declared property (strict makes every property
|
|
106
|
+
* required); otherwise only the required ones: a model leaves an optional parameter it has no reason
|
|
107
|
+
* to set unset, and a placeholder there is a choice nobody made (LibreChat's web_search took an
|
|
108
|
+
* optional `date` enum's first value, "past hour", and its search was refused). A required
|
|
105
109
|
* string is never left empty: an empty value is schema-valid in type but names nothing, so an
|
|
106
110
|
* application's tool could not act on it and a story could not follow the call. It takes what the
|
|
107
111
|
* user's message names (`namedIn`), else a deterministic value of the parameter's own name; an enum
|
|
@@ -113,9 +117,12 @@ export function stubToolArguments(tool: unknown, userText = ''): string {
|
|
|
113
117
|
const props = schema && typeof schema === 'object' ? schema.properties : undefined;
|
|
114
118
|
if (!props || typeof props !== 'object') return '{}';
|
|
115
119
|
const required = new Set(Array.isArray(schema!.required) ? schema!.required.map(String) : []);
|
|
120
|
+
const strict = (tool as { function?: { strict?: unknown }; strict?: unknown } | undefined)?.function?.strict === true || (tool as { strict?: unknown } | undefined)?.strict === true;
|
|
121
|
+
const onlyRequired = !strict && Array.isArray(schema!.required);
|
|
116
122
|
const words = new Set(userText.toLowerCase().match(/[a-z0-9-]+/g) ?? []);
|
|
117
123
|
const out: Record<string, unknown> = {};
|
|
118
124
|
for (const [key, def] of Object.entries(props)) {
|
|
125
|
+
if (onlyRequired && !required.has(key)) continue;
|
|
119
126
|
const d = def as { type?: unknown; enum?: unknown[] } | undefined;
|
|
120
127
|
const named = Array.isArray(d?.enum) ? d!.enum!.find((e) => typeof e === 'string' && words.has(e.toLowerCase())) : undefined;
|
|
121
128
|
out[key] = named ?? (required.has(key) && d?.type === 'string' && !Array.isArray(d?.enum) ? namedIn(userText) ?? `twin-${key}` : placeholderForSchema(def));
|