discoclaw 1.2.4 → 2.0.0
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/.context/voice.md +30 -2
- package/.env.example +7 -3
- package/.env.example.full +13 -32
- package/README.md +1 -1
- package/dist/cli/dashboard.js +7 -1
- package/dist/cli/dashboard.test.js +0 -4
- package/dist/cli/init-wizard.js +4 -8
- package/dist/cli/init-wizard.test.js +4 -10
- package/dist/config.js +5 -38
- package/dist/config.test.js +8 -72
- package/dist/cron/executor.js +72 -1
- package/dist/dashboard/api/metrics.js +7 -0
- package/dist/dashboard/api/metrics.test.js +16 -0
- package/dist/dashboard/api/traces.js +14 -0
- package/dist/dashboard/api/traces.test.js +40 -0
- package/dist/dashboard/page.js +187 -8
- package/dist/dashboard/server.js +82 -19
- package/dist/dashboard/server.test.js +123 -10
- package/dist/discord/actions.js +112 -6
- package/dist/discord/actions.test.js +117 -1
- package/dist/discord/deferred-runner.js +306 -219
- package/dist/discord/help-command.js +1 -1
- package/dist/discord/message-coordinator.js +4 -36
- package/dist/discord/models-command.js +1 -1
- package/dist/discord/reaction-handler.js +83 -5
- package/dist/discord/reaction-handler.test.js +55 -0
- package/dist/discord/verify-push.js +31 -36
- package/dist/discord/verify-push.test.js +34 -6
- package/dist/discord/voice-command.js +1 -31
- package/dist/discord/voice-command.test.js +21 -259
- package/dist/discord/voice-status-command.js +3 -22
- package/dist/discord/voice-status-command.test.js +16 -124
- package/dist/discord-followup.test.js +133 -0
- package/dist/health/config-doctor.js +5 -27
- package/dist/health/config-doctor.test.js +1 -4
- package/dist/index.js +15 -28
- package/dist/observability/trace-store.js +56 -0
- package/dist/observability/trace-utils.js +31 -0
- package/dist/runtime/codex-cli.js +3 -2
- package/dist/runtime/codex-cli.test.js +33 -0
- package/dist/runtime/model-tiers.js +1 -1
- package/dist/runtime/model-tiers.test.js +9 -0
- package/dist/runtime/openai-tool-schemas.js +17 -0
- package/dist/runtime-overrides.js +2 -3
- package/dist/runtime-overrides.test.js +27 -193
- package/dist/tasks/store.js +10 -6
- package/dist/tasks/store.test.js +44 -0
- package/dist/tasks/task-action-executor.test.js +162 -50
- package/dist/tasks/task-action-mutations.js +22 -2
- package/dist/tasks/task-action-read-ops.js +7 -1
- package/dist/tasks/task-action-runner-types.js +19 -1
- package/dist/voice/audio-pipeline.js +183 -96
- package/dist/voice/audio-receiver.js +8 -0
- package/dist/voice/audio-receiver.test.js +16 -0
- package/dist/voice/conversation-buffer.js +16 -6
- package/dist/voice/providers/gemini-live-provider.js +481 -0
- package/dist/voice/providers/gemini-live-provider.test.js +834 -0
- package/dist/voice/providers/gemini-live-responder.js +267 -0
- package/dist/voice/providers/gemini-live-responder.test.js +615 -0
- package/dist/voice/providers/gemini-live-token-estimator.js +100 -0
- package/dist/voice/providers/gemini-live-token-estimator.test.js +160 -0
- package/dist/voice/providers/gemini-live-types.js +32 -0
- package/dist/voice/providers/gemini-tool-mapper.js +91 -0
- package/dist/voice/providers/gemini-tool-mapper.test.js +253 -0
- package/dist/voice/providers/index.js +3 -0
- package/dist/voice/voice-prompt-builder.js +26 -17
- package/dist/voice/voice-prompt-builder.test.js +16 -1
- package/docs/configuration.md +4 -9
- package/docs/official-docs.md +6 -9
- package/docs/runtime-switching.md +1 -1
- package/package.json +1 -1
- package/dist/voice/audio-pipeline.test.js +0 -619
- package/dist/voice/stt-deepgram.js +0 -154
- package/dist/voice/stt-deepgram.test.js +0 -275
- package/dist/voice/stt-factory.js +0 -42
- package/dist/voice/stt-factory.test.js +0 -45
- package/dist/voice/stt-openai.js +0 -156
- package/dist/voice/stt-openai.test.js +0 -281
- package/dist/voice/tts-cartesia.js +0 -169
- package/dist/voice/tts-cartesia.test.js +0 -228
- package/dist/voice/tts-deepgram.js +0 -84
- package/dist/voice/tts-deepgram.test.js +0 -220
- package/dist/voice/tts-factory.js +0 -52
- package/dist/voice/tts-factory.test.js +0 -53
- package/dist/voice/tts-openai.js +0 -70
- package/dist/voice/tts-openai.test.js +0 -138
- package/dist/voice/types.test.js +0 -84
|
@@ -13,7 +13,7 @@ export const KNOWN_RUNTIMES = new Set([
|
|
|
13
13
|
'openai',
|
|
14
14
|
'openrouter',
|
|
15
15
|
]);
|
|
16
|
-
const KNOWN_RUNTIME_OVERRIDE_KEYS = new Set(['
|
|
16
|
+
const KNOWN_RUNTIME_OVERRIDE_KEYS = new Set(['voiceRuntime', 'fastRuntime']);
|
|
17
17
|
const DEPRECATED_ENV_VARS = {
|
|
18
18
|
DISCOCLAW_FAST_RUNTIME: {
|
|
19
19
|
recommendation: "Replace DISCOCLAW_FAST_RUNTIME with '!models set fast <model>' and remove the env var once the runtime override is no longer needed.",
|
|
@@ -146,8 +146,6 @@ async function readRuntimeOverridesFileState(filePath) {
|
|
|
146
146
|
}
|
|
147
147
|
const obj = parsed;
|
|
148
148
|
const overrides = {};
|
|
149
|
-
if (typeof obj['ttsVoice'] === 'string')
|
|
150
|
-
overrides.ttsVoice = obj['ttsVoice'];
|
|
151
149
|
if (typeof obj['voiceRuntime'] === 'string')
|
|
152
150
|
overrides.voiceRuntime = obj['voiceRuntime'];
|
|
153
151
|
if (typeof obj['fastRuntime'] === 'string')
|
|
@@ -568,32 +566,12 @@ export function detectMissingSecrets(ctx) {
|
|
|
568
566
|
}
|
|
569
567
|
const voiceEnabled = parseBoolean(ctx.env.DISCOCLAW_VOICE_ENABLED, false);
|
|
570
568
|
if (voiceEnabled) {
|
|
571
|
-
|
|
572
|
-
const ttsProvider = trimValue(ctx.env.DISCOCLAW_TTS_PROVIDER) ?? 'cartesia';
|
|
573
|
-
const voiceSecretTargets = [];
|
|
574
|
-
if (sttProvider === 'deepgram') {
|
|
575
|
-
voiceSecretTargets.push({ label: 'DISCOCLAW_STT_PROVIDER', provider: sttProvider, secretKey: 'DEEPGRAM_API_KEY' });
|
|
576
|
-
}
|
|
577
|
-
else if (sttProvider === 'openai') {
|
|
578
|
-
voiceSecretTargets.push({ label: 'DISCOCLAW_STT_PROVIDER', provider: sttProvider, secretKey: 'OPENAI_API_KEY' });
|
|
579
|
-
}
|
|
580
|
-
if (ttsProvider === 'cartesia') {
|
|
581
|
-
voiceSecretTargets.push({ label: 'DISCOCLAW_TTS_PROVIDER', provider: ttsProvider, secretKey: 'CARTESIA_API_KEY' });
|
|
582
|
-
}
|
|
583
|
-
else if (ttsProvider === 'deepgram') {
|
|
584
|
-
voiceSecretTargets.push({ label: 'DISCOCLAW_TTS_PROVIDER', provider: ttsProvider, secretKey: 'DEEPGRAM_API_KEY' });
|
|
585
|
-
}
|
|
586
|
-
else if (ttsProvider === 'openai') {
|
|
587
|
-
voiceSecretTargets.push({ label: 'DISCOCLAW_TTS_PROVIDER', provider: ttsProvider, secretKey: 'OPENAI_API_KEY' });
|
|
588
|
-
}
|
|
589
|
-
for (const target of voiceSecretTargets) {
|
|
590
|
-
if (hasSecret(ctx.env, target.secretKey))
|
|
591
|
-
continue;
|
|
569
|
+
if (!hasSecret(ctx.env, 'GEMINI_API_KEY')) {
|
|
592
570
|
findings.push({
|
|
593
|
-
id:
|
|
571
|
+
id: 'missing-secret:DISCOCLAW_VOICE_ENABLED:GEMINI_API_KEY',
|
|
594
572
|
severity: 'error',
|
|
595
|
-
message:
|
|
596
|
-
recommendation:
|
|
573
|
+
message: 'DISCOCLAW_VOICE_ENABLED=1 requires GEMINI_API_KEY, but it is not set.',
|
|
574
|
+
recommendation: 'Set GEMINI_API_KEY or disable voice on this install.',
|
|
597
575
|
autoFixable: false,
|
|
598
576
|
});
|
|
599
577
|
}
|
|
@@ -242,8 +242,6 @@ describe('detectMissingSecrets', () => {
|
|
|
242
242
|
await writeEnv(cwd, [
|
|
243
243
|
'PRIMARY_RUNTIME=openrouter',
|
|
244
244
|
'DISCOCLAW_VOICE_ENABLED=1',
|
|
245
|
-
'DISCOCLAW_STT_PROVIDER=deepgram',
|
|
246
|
-
'DISCOCLAW_TTS_PROVIDER=cartesia',
|
|
247
245
|
'DISCOCLAW_COLD_STORAGE_ENABLED=1',
|
|
248
246
|
'DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=1',
|
|
249
247
|
'IMAGEGEN_DEFAULT_MODEL=imagen-4.0-generate-001',
|
|
@@ -256,8 +254,7 @@ describe('detectMissingSecrets', () => {
|
|
|
256
254
|
expect(findings.map((finding) => finding.id)).toEqual([
|
|
257
255
|
'missing-secret:PRIMARY_RUNTIME:OPENROUTER_API_KEY',
|
|
258
256
|
'missing-secret:runtime-overrides.voiceRuntime:OPENAI_API_KEY',
|
|
259
|
-
'missing-secret:
|
|
260
|
-
'missing-secret:DISCOCLAW_TTS_PROVIDER:CARTESIA_API_KEY',
|
|
257
|
+
'missing-secret:DISCOCLAW_VOICE_ENABLED:GEMINI_API_KEY',
|
|
261
258
|
'missing-secret:DISCOCLAW_COLD_STORAGE_ENABLED:COLD_STORAGE_API_KEY-or-OPENAI_API_KEY',
|
|
262
259
|
'missing-secret:DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN:OPENAI_API_KEY-or-IMAGEGEN_GEMINI_API_KEY',
|
|
263
260
|
'missing-secret:IMAGEGEN_DEFAULT_MODEL:IMAGEGEN_GEMINI_API_KEY',
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { buildDurableMemorySection } from './discord/prompt-common.js';
|
|
|
19
19
|
import { parseDiscordActions, executeDiscordActions, buildTieredDiscordActionsPromptSection, buildAllResultLines, appendActionResults } from './discord/actions.js';
|
|
20
20
|
import { DiscordTransportClient } from './discord/transport-client.js';
|
|
21
21
|
import { buildVoiceActionFlags } from './voice/voice-action-flags.js';
|
|
22
|
-
import { loadVoiceIdentity, buildVoicePrompt, buildVoiceFollowUpPrompt, buildVoicePromptSectionEstimates } from './voice/voice-prompt-builder.js';
|
|
22
|
+
import { loadVoiceIdentity, buildVoicePrompt, buildVoiceFollowUpPrompt, buildVoicePromptSectionEstimates, buildVoiceSystemInstruction, } from './voice/voice-prompt-builder.js';
|
|
23
23
|
import { sanitizeForVoice, sanitizeVoiceReplyForSpeech } from './voice/voice-sanitize.js';
|
|
24
24
|
import { shouldTriggerFollowUp } from './discord/action-categories.js';
|
|
25
25
|
import { configureDeferredScheduler } from './discord/deferred-runner.js';
|
|
@@ -1105,9 +1105,6 @@ if (currentOverridesState.fastRuntime) {
|
|
|
1105
1105
|
log.warn({ fastRuntime: currentOverridesState.fastRuntime, availableRuntimes: runtimeRegistry.list() }, 'runtime-overrides: fastRuntime is not a registered runtime; ignoring');
|
|
1106
1106
|
}
|
|
1107
1107
|
}
|
|
1108
|
-
if (currentOverridesState.ttsVoice) {
|
|
1109
|
-
log.info({ ttsVoice: currentOverridesState.ttsVoice }, 'runtime-overrides: ttsVoice override will be applied');
|
|
1110
|
-
}
|
|
1111
1108
|
// Track which roles have active file-backed overrides (used by !models show).
|
|
1112
1109
|
// Only mark as override if the stored value differs from env defaults.
|
|
1113
1110
|
const overrideSources = detectOverrideSources(currentModelConfig, envModelDefaults);
|
|
@@ -1261,8 +1258,6 @@ const botParams = {
|
|
|
1261
1258
|
spawnCtx: undefined,
|
|
1262
1259
|
voiceCtx: undefined,
|
|
1263
1260
|
voiceStatusCtx: undefined,
|
|
1264
|
-
setTtsVoice: undefined,
|
|
1265
|
-
getTtsVoice: undefined,
|
|
1266
1261
|
configCtx: undefined,
|
|
1267
1262
|
deferOpts: undefined,
|
|
1268
1263
|
messageHistoryBudget,
|
|
@@ -1325,13 +1320,8 @@ const botParams = {
|
|
|
1325
1320
|
voiceEnabled: cfg.voiceEnabled,
|
|
1326
1321
|
voiceAutoJoin: cfg.voiceAutoJoin,
|
|
1327
1322
|
voiceModelCtx: voiceModelRef,
|
|
1328
|
-
voiceSttProvider: cfg.voiceSttProvider,
|
|
1329
|
-
voiceTtsProvider: cfg.voiceTtsProvider,
|
|
1330
1323
|
voiceHomeChannel: cfg.voiceHomeChannel,
|
|
1331
|
-
|
|
1332
|
-
deepgramSttModel: cfg.deepgramSttModel,
|
|
1333
|
-
deepgramTtsVoice: cfg.deepgramTtsVoice,
|
|
1334
|
-
cartesiaApiKey: cfg.cartesiaApiKey,
|
|
1324
|
+
geminiApiKey: cfg.geminiApiKey,
|
|
1335
1325
|
botStatus: cfg.botStatus,
|
|
1336
1326
|
botActivity: cfg.botActivity,
|
|
1337
1327
|
botActivityType: cfg.botActivityType,
|
|
@@ -2077,30 +2067,35 @@ if (taskCtx) {
|
|
|
2077
2067
|
}
|
|
2078
2068
|
};
|
|
2079
2069
|
}
|
|
2070
|
+
const buildGeminiSystemInstruction = async () => {
|
|
2071
|
+
const identity = await loadVoiceIdentity(workspaceCwd);
|
|
2072
|
+
return buildVoiceSystemInstruction({
|
|
2073
|
+
identity,
|
|
2074
|
+
durableMemory: '',
|
|
2075
|
+
voiceSystemPrompt: cfg.voiceSystemPrompt,
|
|
2076
|
+
actionsSection: '',
|
|
2077
|
+
});
|
|
2078
|
+
};
|
|
2080
2079
|
audioPipeline = new AudioPipelineManager({
|
|
2081
2080
|
log,
|
|
2082
2081
|
voiceConfig: {
|
|
2083
2082
|
enabled: cfg.voiceEnabled,
|
|
2084
|
-
sttProvider: cfg.voiceSttProvider,
|
|
2085
|
-
ttsProvider: cfg.voiceTtsProvider,
|
|
2086
2083
|
homeChannel: cfg.voiceHomeChannel,
|
|
2087
|
-
deepgramApiKey: cfg.deepgramApiKey,
|
|
2088
|
-
deepgramSttModel: cfg.deepgramSttModel,
|
|
2089
|
-
deepgramTtsVoice: overrides.ttsVoice ?? cfg.deepgramTtsVoice,
|
|
2090
|
-
deepgramTtsSpeed: cfg.deepgramTtsSpeed,
|
|
2091
|
-
cartesiaApiKey: cfg.cartesiaApiKey,
|
|
2092
|
-
openaiApiKey: cfg.openaiApiKey,
|
|
2093
2084
|
},
|
|
2094
2085
|
allowedUserIds: allowUserIds,
|
|
2095
2086
|
createDecoder: opusDecoderFactory,
|
|
2087
|
+
geminiApiKey: cfg.geminiApiKey,
|
|
2088
|
+
enabledTools: runtimeTools,
|
|
2096
2089
|
invokeAi: voiceInvokeAi,
|
|
2097
2090
|
runtime: voiceRuntimeRef.runtime.id,
|
|
2098
2091
|
runtimeModel: voiceModelRef.model,
|
|
2099
2092
|
runtimeCwd: workspaceCwd,
|
|
2100
2093
|
runtimeTimeoutMs,
|
|
2094
|
+
sessionRotationMs: cfg.geminiSessionRotationMs,
|
|
2101
2095
|
transcriptMirror,
|
|
2102
2096
|
botDisplayName,
|
|
2103
2097
|
backfill,
|
|
2098
|
+
buildGeminiSystemInstruction,
|
|
2104
2099
|
onTranscription: (guildId, result) => {
|
|
2105
2100
|
if (result.isFinal && result.text.trim()) {
|
|
2106
2101
|
log.info({ guildId, text: result.text, confidence: result.confidence }, 'voice:transcription');
|
|
@@ -2120,14 +2115,6 @@ if (taskCtx) {
|
|
|
2120
2115
|
},
|
|
2121
2116
|
});
|
|
2122
2117
|
botParams.voiceStatusCtx = { voiceManager };
|
|
2123
|
-
botParams.setTtsVoice = async (voice) => {
|
|
2124
|
-
const count = await audioPipeline.setTtsVoice(voice);
|
|
2125
|
-
botParams.deepgramTtsVoice = voice;
|
|
2126
|
-
currentOverridesState.ttsVoice = voice;
|
|
2127
|
-
saveOverrides(overridesPath, currentOverridesState).catch((err) => log.warn({ err, voice }, 'runtime-overrides: ttsVoice save failed'));
|
|
2128
|
-
return count;
|
|
2129
|
-
};
|
|
2130
|
-
botParams.getTtsVoice = () => audioPipeline.ttsVoice;
|
|
2131
2118
|
if (cfg.discordActionsVoice) {
|
|
2132
2119
|
botParams.voiceCtx = { voiceManager };
|
|
2133
2120
|
log.info('voice:action context initialized with audio pipeline');
|
|
@@ -88,6 +88,62 @@ export class TraceStore {
|
|
|
88
88
|
.slice(0, limit)
|
|
89
89
|
.map(cloneTrace);
|
|
90
90
|
}
|
|
91
|
+
get size() {
|
|
92
|
+
return this.traces.size;
|
|
93
|
+
}
|
|
94
|
+
summary() {
|
|
95
|
+
const allTraces = [...this.traces.values()];
|
|
96
|
+
const flows = ['message', 'reaction', 'cron', 'defer'];
|
|
97
|
+
const byFlow = {};
|
|
98
|
+
for (const flow of flows) {
|
|
99
|
+
const matching = allTraces.filter((t) => t.flow === flow);
|
|
100
|
+
const completed = matching.filter((t) => t.outcome !== 'in_progress');
|
|
101
|
+
const succeeded = completed.filter((t) => t.outcome === 'success').length;
|
|
102
|
+
const totalDuration = completed.reduce((sum, t) => sum + t.durationMs, 0);
|
|
103
|
+
byFlow[flow] = {
|
|
104
|
+
total: matching.length,
|
|
105
|
+
succeeded,
|
|
106
|
+
failed: completed.length - succeeded,
|
|
107
|
+
inProgress: matching.length - completed.length,
|
|
108
|
+
avgDurationMs: completed.length > 0 ? Math.round(totalDuration / completed.length) : 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const byOutcome = {};
|
|
112
|
+
for (const trace of allTraces) {
|
|
113
|
+
byOutcome[trace.outcome] = (byOutcome[trace.outcome] ?? 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
const MAX_RECENT_ERRORS = 10;
|
|
116
|
+
const recentErrors = [];
|
|
117
|
+
const sorted = [...allTraces].sort((a, b) => b.startedAt - a.startedAt);
|
|
118
|
+
for (const trace of sorted) {
|
|
119
|
+
if (recentErrors.length >= MAX_RECENT_ERRORS)
|
|
120
|
+
break;
|
|
121
|
+
if (trace.outcome === 'success' || trace.outcome === 'in_progress')
|
|
122
|
+
continue;
|
|
123
|
+
const lastError = [...trace.events].reverse().find((e) => e.type === 'error');
|
|
124
|
+
recentErrors.push({
|
|
125
|
+
traceId: trace.traceId,
|
|
126
|
+
flow: trace.flow,
|
|
127
|
+
message: lastError && 'message' in lastError ? lastError.message : trace.outcome,
|
|
128
|
+
at: trace.startedAt,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
let oldestAt = null;
|
|
132
|
+
let newestAt = null;
|
|
133
|
+
if (allTraces.length > 0) {
|
|
134
|
+
oldestAt = Math.min(...allTraces.map((t) => t.startedAt));
|
|
135
|
+
newestAt = Math.max(...allTraces.map((t) => t.startedAt));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
total: allTraces.length,
|
|
139
|
+
inProgress: allTraces.filter((t) => t.outcome === 'in_progress').length,
|
|
140
|
+
oldestAt,
|
|
141
|
+
newestAt,
|
|
142
|
+
byFlow,
|
|
143
|
+
byOutcome,
|
|
144
|
+
recentErrors,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
91
147
|
makeRoomForNewTrace() {
|
|
92
148
|
this.pruneToLimit(this.maxEntries - 1);
|
|
93
149
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared trace-event helpers for summarizing values before they enter trace events.
|
|
3
|
+
*/
|
|
4
|
+
export function summarizeTraceText(value, maxChars = 160) {
|
|
5
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (normalized.length <= maxChars) {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
return `${normalized.slice(0, Math.max(1, maxChars - 1))}…`;
|
|
13
|
+
}
|
|
14
|
+
export function summarizeTraceValue(value, maxChars = 160) {
|
|
15
|
+
if (value == null) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return summarizeTraceText(value, maxChars);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const serialized = JSON.stringify(value);
|
|
23
|
+
if (serialized) {
|
|
24
|
+
return summarizeTraceText(serialized, maxChars);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fall through to String(value).
|
|
29
|
+
}
|
|
30
|
+
return summarizeTraceText(String(value), maxChars);
|
|
31
|
+
}
|
|
@@ -21,7 +21,8 @@ function mergeSystemPrompt(systemPrompt, appendSystemPrompt) {
|
|
|
21
21
|
function normalizeInvokeParams(params, opts) {
|
|
22
22
|
const requestedModel = params.model || opts.defaultModel;
|
|
23
23
|
const remappedModel = remapCrossRuntimeTierModel(requestedModel, 'codex');
|
|
24
|
-
const
|
|
24
|
+
const normalizedModel = remappedModel?.model ?? requestedModel;
|
|
25
|
+
const effectiveModel = normalizedModel || opts.defaultModel;
|
|
25
26
|
const effectiveReasoningEffort = params.reasoningEffort
|
|
26
27
|
?? (remappedModel ? resolveReasoningEffort(remappedModel.sourceTier, 'codex') : undefined);
|
|
27
28
|
if (remappedModel) {
|
|
@@ -34,7 +35,7 @@ function normalizeInvokeParams(params, opts) {
|
|
|
34
35
|
}
|
|
35
36
|
return {
|
|
36
37
|
...params,
|
|
37
|
-
model:
|
|
38
|
+
model: normalizedModel,
|
|
38
39
|
...(effectiveReasoningEffort ? { reasoningEffort: effectiveReasoningEffort } : {}),
|
|
39
40
|
systemPrompt: mergeSystemPrompt(params.systemPrompt, opts.appendSystemPrompt),
|
|
40
41
|
...(opts.disableSessions ? { sessionKey: undefined } : {}),
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { CODEX_RUNTIME_CAPABILITIES } from './tool-capabilities.js';
|
|
5
|
+
import { initTierOverrides } from './model-tiers.js';
|
|
5
6
|
const { mockExeca } = vi.hoisted(() => {
|
|
6
7
|
return {
|
|
7
8
|
mockExeca: vi.fn(),
|
|
@@ -129,10 +130,12 @@ describe('Codex CLI runtime adapter', () => {
|
|
|
129
130
|
const originalStableHome = process.env.DISCOCLAW_CODEX_STABLE_HOME;
|
|
130
131
|
beforeEach(() => {
|
|
131
132
|
mockExeca.mockReset();
|
|
133
|
+
initTierOverrides({});
|
|
132
134
|
delete process.env.DISCOCLAW_CLI_LAUNCHER_STATE_HARDENING;
|
|
133
135
|
delete process.env.DISCOCLAW_CODEX_STABLE_HOME;
|
|
134
136
|
});
|
|
135
137
|
afterEach(() => {
|
|
138
|
+
initTierOverrides({});
|
|
136
139
|
if (originalHardening === undefined) {
|
|
137
140
|
delete process.env.DISCOCLAW_CLI_LAUNCHER_STATE_HARDENING;
|
|
138
141
|
}
|
|
@@ -294,6 +297,36 @@ describe('Codex CLI runtime adapter', () => {
|
|
|
294
297
|
sourceTier: 'fast',
|
|
295
298
|
}), 'codex:model remapped to codex-compatible tier default');
|
|
296
299
|
});
|
|
300
|
+
it('falls back to the codex default model when the codex fast tier is configured as adapter-default sentinel', async () => {
|
|
301
|
+
initTierOverrides({ DISCOCLAW_TIER_CODEX_FAST: '' });
|
|
302
|
+
const log = {
|
|
303
|
+
debug: vi.fn(),
|
|
304
|
+
warn: vi.fn(),
|
|
305
|
+
};
|
|
306
|
+
mockExeca.mockReturnValue(createMockSubprocess({
|
|
307
|
+
stdout: 'cli remap ok',
|
|
308
|
+
exitCode: 0,
|
|
309
|
+
}));
|
|
310
|
+
const rt = createCodexCliRuntime({
|
|
311
|
+
codexBin: 'codex',
|
|
312
|
+
defaultModel: 'gpt-5.4',
|
|
313
|
+
log,
|
|
314
|
+
});
|
|
315
|
+
await collectEvents(rt.invoke({
|
|
316
|
+
prompt: 'Summarize',
|
|
317
|
+
model: 'gpt-5-mini',
|
|
318
|
+
cwd: '/tmp/non-default-cwd',
|
|
319
|
+
}));
|
|
320
|
+
const callArgs = mockExeca.mock.calls[0][1];
|
|
321
|
+
const modelIdx = callArgs.indexOf('-m');
|
|
322
|
+
expect(callArgs[modelIdx + 1]).toBe('gpt-5.4');
|
|
323
|
+
expect(log.warn).toHaveBeenCalledWith(expect.objectContaining({
|
|
324
|
+
requestedModel: 'gpt-5-mini',
|
|
325
|
+
effectiveModel: 'gpt-5.4',
|
|
326
|
+
sourceRuntimeId: 'openai',
|
|
327
|
+
sourceTier: 'fast',
|
|
328
|
+
}), 'codex:model remapped to codex-compatible tier default');
|
|
329
|
+
});
|
|
297
330
|
it('large prompt uses stdin instead of positional arg', async () => {
|
|
298
331
|
const largePrompt = 'x'.repeat(200_000);
|
|
299
332
|
mockExeca.mockReturnValue(createMockSubprocess({
|
|
@@ -168,7 +168,7 @@ export function remapCrossRuntimeTierModel(model, targetRuntimeId) {
|
|
|
168
168
|
if (!sourceTier)
|
|
169
169
|
return null;
|
|
170
170
|
const targetModel = tierMap[targetRuntimeId]?.[sourceTier];
|
|
171
|
-
if (
|
|
171
|
+
if (targetModel === undefined || normalizeModelLookup(targetModel) === normalized)
|
|
172
172
|
return null;
|
|
173
173
|
return {
|
|
174
174
|
sourceRuntimeId,
|
|
@@ -238,6 +238,15 @@ describe('remapCrossRuntimeTierModel', () => {
|
|
|
238
238
|
model: 'gpt-5.4',
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
|
+
it('preserves adapter-default sentinels when the target runtime tier is intentionally blank', () => {
|
|
242
|
+
initTierOverrides({ DISCOCLAW_TIER_CODEX_FAST: '' });
|
|
243
|
+
expect(remapCrossRuntimeTierModel('gpt-5-mini', 'codex')).toEqual({
|
|
244
|
+
sourceRuntimeId: 'openai',
|
|
245
|
+
sourceTier: 'fast',
|
|
246
|
+
targetRuntimeId: 'codex',
|
|
247
|
+
model: '',
|
|
248
|
+
});
|
|
249
|
+
});
|
|
241
250
|
it('does not remap ambiguous cross-runtime models', () => {
|
|
242
251
|
expect(remapCrossRuntimeTierModel('gpt-5.4', 'codex')).toBeNull();
|
|
243
252
|
});
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Maps internal tool names (Read, Write, …) to OpenAI function names
|
|
5
5
|
* (read_file, write_file, …) and provides JSON Schema parameter definitions.
|
|
6
|
+
*
|
|
7
|
+
* Also provides `buildGeminiToolDeclarations()` to produce Gemini-compatible
|
|
8
|
+
* function declarations for the Multimodal Live API (Phase 2.1).
|
|
6
9
|
*/
|
|
10
|
+
import { toGeminiTools } from '../voice/providers/gemini-tool-mapper.js';
|
|
7
11
|
/** Discoclaw tool name → OpenAI function names */
|
|
8
12
|
const DISCO_TO_OPENAI_NAMES = {
|
|
9
13
|
Read: ['read_file'],
|
|
@@ -369,3 +373,16 @@ export function buildToolSchemas(enabledTools) {
|
|
|
369
373
|
}
|
|
370
374
|
return schemas;
|
|
371
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Build Gemini-compatible function declarations for the given enabled tools.
|
|
378
|
+
*
|
|
379
|
+
* Composes `buildToolSchemas()` → `toGeminiTools()` so callers (e.g.
|
|
380
|
+
* AudioPipelineManager) can get Gemini Live–ready declarations in one call.
|
|
381
|
+
* Returns `undefined` when no tools match (same semantics as `toGeminiTools`).
|
|
382
|
+
*/
|
|
383
|
+
export function buildGeminiToolDeclarations(enabledTools, opts = {}) {
|
|
384
|
+
const schemas = buildToolSchemas(enabledTools);
|
|
385
|
+
return toGeminiTools(schemas, opts.nonBlocking
|
|
386
|
+
? { nonBlockingFunctionNames: schemas.map((schema) => schema.function.name) }
|
|
387
|
+
: undefined);
|
|
388
|
+
}
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseRuntimeNameForPlacement } from './runtime/runtime-path-contract.js';
|
|
4
4
|
const OVERRIDES_FILENAME = 'runtime-overrides.json';
|
|
5
|
-
const KNOWN_OVERRIDE_KEYS = ['
|
|
5
|
+
const KNOWN_OVERRIDE_KEYS = ['voiceRuntime', 'fastRuntime'];
|
|
6
6
|
export function normalizeRuntimeOverrides(overrides) {
|
|
7
7
|
const next = { ...overrides };
|
|
8
8
|
let changed = false;
|
|
@@ -57,8 +57,6 @@ export async function loadOverrides(filePath, onWarn) {
|
|
|
57
57
|
}
|
|
58
58
|
const obj = parsed;
|
|
59
59
|
const overrides = {};
|
|
60
|
-
if (typeof obj['ttsVoice'] === 'string')
|
|
61
|
-
overrides.ttsVoice = obj['ttsVoice'];
|
|
62
60
|
if (typeof obj['voiceRuntime'] === 'string')
|
|
63
61
|
overrides.voiceRuntime = obj['voiceRuntime'];
|
|
64
62
|
if (typeof obj['fastRuntime'] === 'string')
|
|
@@ -75,6 +73,7 @@ export async function saveOverrides(filePath, overrides) {
|
|
|
75
73
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
76
74
|
try {
|
|
77
75
|
const preserved = await readExistingRawObject(filePath);
|
|
76
|
+
delete preserved['ttsVoice'];
|
|
78
77
|
for (const key of KNOWN_OVERRIDE_KEYS) {
|
|
79
78
|
delete preserved[key];
|
|
80
79
|
}
|