memorix 1.9.7 → 1.9.9
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/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/cli/index.js +192 -11
- package/dist/index.js +184 -11
- package/dist/maintenance-runner.js +22 -5
- package/dist/media-audio-runner.js +22 -5
- package/dist/media-video-runner.js +22 -5
- package/dist/memcode-runtime/CHANGELOG.md +15 -0
- package/dist/sdk.js +183 -10
- package/dist/vector-backfill-runner.js +22 -5
- package/docs/CONFIGURATION.md +22 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/README.md +1 -1
- package/docs/SETUP.md +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/init.ts +7 -1
- package/src/cli/tui/ConfigureView.tsx +2 -0
- package/src/config/dotenv-loader.ts +1 -0
- package/src/config/resolved-config.ts +19 -5
- package/src/config/yaml-loader.ts +2 -2
- package/src/config.ts +5 -1
- package/src/dashboard/server.ts +3 -0
- package/src/hooks/installers/index.ts +160 -3
- package/src/llm/provider.ts +2 -1
- package/src/multimodal/image-loader.ts +2 -2
|
@@ -96,6 +96,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
96
96
|
const legacy = loadFileConfig(homeDir);
|
|
97
97
|
const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
|
|
98
98
|
const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : undefined;
|
|
99
|
+
const requestyEmbeddingApiKey = isRequestyUrl(embeddingBaseUrl) ? process.env.REQUESTY_API_KEY : undefined;
|
|
99
100
|
const memoryLlmProvider = first(
|
|
100
101
|
process.env.MEMORIX_LLM_PROVIDER,
|
|
101
102
|
toml.memory?.llm?.provider,
|
|
@@ -118,16 +119,18 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
118
119
|
? process.env.OPENROUTER_API_KEY
|
|
119
120
|
: undefined;
|
|
120
121
|
const isAtlasCloud = memoryLlmProvider === 'atlascloud';
|
|
122
|
+
const isRequesty = memoryLlmProvider === 'requesty';
|
|
123
|
+
const usesPresetKeyOnly = isAtlasCloud || isRequesty;
|
|
121
124
|
const memoryLlmApiKey = first(
|
|
122
125
|
process.env.MEMORIX_LLM_API_KEY,
|
|
123
126
|
process.env.MEMORIX_API_KEY,
|
|
124
127
|
toml.memory?.llm?.api_key,
|
|
125
128
|
yaml.llm?.apiKey,
|
|
126
129
|
legacy.llm?.apiKey,
|
|
127
|
-
// Never borrow another provider's credentials for the Atlas Cloud
|
|
128
|
-
isAtlasCloud ? process.env.ATLASCLOUD_API_KEY : process.env.OPENAI_API_KEY,
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
// Never borrow another provider's credentials for the Atlas Cloud or Requesty presets.
|
|
131
|
+
isAtlasCloud ? process.env.ATLASCLOUD_API_KEY : isRequesty ? process.env.REQUESTY_API_KEY : process.env.OPENAI_API_KEY,
|
|
132
|
+
usesPresetKeyOnly ? undefined : process.env.ANTHROPIC_API_KEY,
|
|
133
|
+
usesPresetKeyOnly ? undefined : openRouterMemoryLlmApiKey,
|
|
131
134
|
);
|
|
132
135
|
|
|
133
136
|
const resolved: ResolvedMemorixConfig = {
|
|
@@ -177,7 +180,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
177
180
|
provider: first(process.env.MEMORIX_EMBEDDING, toml.embedding?.provider, yaml.embedding?.provider, legacy.embedding, 'off'),
|
|
178
181
|
model: first(process.env.MEMORIX_EMBEDDING_MODEL, toml.embedding?.model, yaml.embedding?.model, legacy.embeddingApi?.model),
|
|
179
182
|
baseUrl: embeddingBaseUrl,
|
|
180
|
-
apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey),
|
|
183
|
+
apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey, requestyEmbeddingApiKey),
|
|
181
184
|
dimensions: firstNumber(parseNumber(process.env.MEMORIX_EMBEDDING_DIMENSIONS), toml.embedding?.dimensions, yaml.embedding?.dimensions, legacy.embeddingApi?.dimensions),
|
|
182
185
|
},
|
|
183
186
|
rerank: resolveRerankLane({
|
|
@@ -323,6 +326,7 @@ function getEnvSourceNames(): string[] {
|
|
|
323
326
|
'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
|
|
324
327
|
'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
|
|
325
328
|
'OPENROUTER_API_KEY',
|
|
329
|
+
'REQUESTY_API_KEY',
|
|
326
330
|
'ATLASCLOUD_API_KEY',
|
|
327
331
|
].filter((name) => process.env[name]);
|
|
328
332
|
}
|
|
@@ -337,6 +341,16 @@ function isOpenRouterUrl(value: string | undefined): boolean {
|
|
|
337
341
|
}
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
function isRequestyUrl(value: string | undefined): boolean {
|
|
345
|
+
if (!value) return false;
|
|
346
|
+
try {
|
|
347
|
+
const url = new URL(value);
|
|
348
|
+
return url.hostname.toLowerCase() === 'requesty.ai' || url.hostname.toLowerCase().endsWith('.requesty.ai');
|
|
349
|
+
} catch {
|
|
350
|
+
return /(^|\.)requesty\.ai(?::|\/|$)/i.test(value);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
340
354
|
function isOpenRouterMemoryLane(provider: string | undefined, baseUrl: string | undefined): boolean {
|
|
341
355
|
return provider?.trim().toLowerCase() === 'openrouter' || isOpenRouterUrl(baseUrl);
|
|
342
356
|
}
|
|
@@ -25,7 +25,7 @@ import { getGlobalYamlPath, getProjectYamlPath } from './config-paths.js';
|
|
|
25
25
|
export interface MemorixYamlConfig {
|
|
26
26
|
/** LLM provider configuration */
|
|
27
27
|
llm?: {
|
|
28
|
-
provider?: 'openai' | 'anthropic' | 'openrouter' | string;
|
|
28
|
+
provider?: 'openai' | 'anthropic' | 'openrouter' | 'requesty' | string;
|
|
29
29
|
model?: string;
|
|
30
30
|
apiKey?: string;
|
|
31
31
|
baseUrl?: string;
|
|
@@ -33,7 +33,7 @@ export interface MemorixYamlConfig {
|
|
|
33
33
|
|
|
34
34
|
/** TUI / chat agent LLM provider configuration */
|
|
35
35
|
agent?: {
|
|
36
|
-
provider?: 'openai' | 'anthropic' | 'openrouter' | string;
|
|
36
|
+
provider?: 'openai' | 'anthropic' | 'openrouter' | 'requesty' | string;
|
|
37
37
|
model?: string;
|
|
38
38
|
apiKey?: string;
|
|
39
39
|
baseUrl?: string;
|
package/src/config.ts
CHANGED
|
@@ -93,7 +93,8 @@ export function getEmbeddingModel(): string {
|
|
|
93
93
|
/**
|
|
94
94
|
* Provider-aware embedding model default: OpenRouter serves many providers,
|
|
95
95
|
* and the OpenAI-only default does not fit it. Qwen3-Embedding-8B is a
|
|
96
|
-
* strong multilingual default there
|
|
96
|
+
* strong multilingual default there. Requesty expects vendor-prefixed ids,
|
|
97
|
+
* so it gets the prefixed OpenAI model. Everywhere else keep the OpenAI model.
|
|
97
98
|
* An explicitly configured model always wins.
|
|
98
99
|
*/
|
|
99
100
|
export function defaultEmbeddingModelFor(
|
|
@@ -106,6 +107,9 @@ export function defaultEmbeddingModelFor(
|
|
|
106
107
|
if (url && (url.hostname === 'openrouter.ai' || url.hostname.endsWith('.openrouter.ai'))) {
|
|
107
108
|
return 'qwen/qwen3-embedding-8b';
|
|
108
109
|
}
|
|
110
|
+
if (url && (url.hostname === 'requesty.ai' || url.hostname.endsWith('.requesty.ai'))) {
|
|
111
|
+
return 'openai/text-embedding-3-small';
|
|
112
|
+
}
|
|
109
113
|
} catch { /* treat unparsable base URLs as non-OpenRouter */ }
|
|
110
114
|
return 'text-embedding-3-small';
|
|
111
115
|
}
|
package/src/dashboard/server.ts
CHANGED
|
@@ -682,6 +682,9 @@ async function handleApi(
|
|
|
682
682
|
...(resolved.memory.llm.provider === 'openrouter' || resolved.memory.llm.baseUrl?.includes('openrouter.ai')
|
|
683
683
|
? ['OPENROUTER_API_KEY']
|
|
684
684
|
: []),
|
|
685
|
+
...(resolved.memory.llm.provider === 'requesty'
|
|
686
|
+
? ['REQUESTY_API_KEY']
|
|
687
|
+
: []),
|
|
685
688
|
];
|
|
686
689
|
if (llmKey) {
|
|
687
690
|
const src = resolveDashboardKeySource({ value: llmKey, envKeys: memoryKeyEnv, configSource });
|
|
@@ -449,8 +449,10 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
|
|
|
449
449
|
* Format: .opencode/plugins/memorix.js — Bun-compatible JS module
|
|
450
450
|
* See: https://opencode.ai/docs/plugins/
|
|
451
451
|
*
|
|
452
|
-
* Plugin
|
|
453
|
-
* -
|
|
452
|
+
* Plugin contracts:
|
|
453
|
+
* - OpenCode v1: named export `MemorixPlugin` returns an event-hook map.
|
|
454
|
+
* - OpenCode v2: default export contains `id` and `setup(ctx)`, with event
|
|
455
|
+
* subscriptions and tool hooks registered through the context.
|
|
454
456
|
* - Return object keys are EVENT NAMES (e.g. "session.created", "file.edited")
|
|
455
457
|
* - Each key maps to an async handler: (input, output) => { ... }
|
|
456
458
|
* - Session/file/command events: handler receives ({ event }) for event-style hooks
|
|
@@ -464,7 +466,7 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
|
|
|
464
466
|
* protocol used by all agents. spawnSync works in both Node.js and Bun
|
|
465
467
|
* runtimes (OpenCode may fall back to Node.js on Windows).
|
|
466
468
|
*/
|
|
467
|
-
const OPENCODE_PLUGIN_VERSION =
|
|
469
|
+
const OPENCODE_PLUGIN_VERSION = 8;
|
|
468
470
|
|
|
469
471
|
const AGENT_SKILL_DIRS: Partial<Record<AgentName, { project: string; global?: string }>> = {
|
|
470
472
|
cursor: { project: path.join('.cursor', 'skills'), global: path.join('.cursor', 'skills') },
|
|
@@ -716,6 +718,161 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
|
|
|
716
718
|
},
|
|
717
719
|
};
|
|
718
720
|
};
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* OpenCode v2 compatibility layer. Keep this beside the v1 export so one
|
|
724
|
+
* generated file can be loaded by both host generations during migration.
|
|
725
|
+
*/
|
|
726
|
+
const v2HookCommand = ${hookCommand};
|
|
727
|
+
|
|
728
|
+
function v2ReportHookFailure(eventName, detail) {
|
|
729
|
+
if (process.env.MEMORIX_HOOK_DEBUG !== '1') return;
|
|
730
|
+
console.error('[memorix-plugin] v2 hook delivery failed:', eventName, detail);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function v2EventData(event) {
|
|
734
|
+
return event && typeof event.data === 'object' && event.data !== null ? event.data : {};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function v2SessionId(event) {
|
|
738
|
+
const data = v2EventData(event);
|
|
739
|
+
const value = data.sessionID ?? data.sessionId ?? data.session_id
|
|
740
|
+
?? event?.sessionID ?? event?.sessionId;
|
|
741
|
+
return typeof value === 'string' ? value : '';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function v2RunHook(payload, directory, sessionId) {
|
|
745
|
+
const nextPayload = { agent: 'opencode', cwd: directory, ...payload };
|
|
746
|
+
if (sessionId) nextPayload.session_id = sessionId;
|
|
747
|
+
const eventName = nextPayload.hook_event_name || 'unknown';
|
|
748
|
+
try {
|
|
749
|
+
const result = spawnSync(v2HookCommand, ['hook'], {
|
|
750
|
+
input: JSON.stringify(nextPayload),
|
|
751
|
+
timeout: 10_000,
|
|
752
|
+
encoding: 'utf-8',
|
|
753
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
754
|
+
shell: process.platform === 'win32',
|
|
755
|
+
windowsHide: true,
|
|
756
|
+
});
|
|
757
|
+
if (result.status !== 0) {
|
|
758
|
+
v2ReportHookFailure(eventName, {
|
|
759
|
+
exit: result.status,
|
|
760
|
+
stderr: (result.stderr || '').slice(0, 200),
|
|
761
|
+
error: result.error?.message,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
} catch (error) {
|
|
765
|
+
v2ReportHookFailure(eventName, error?.message ?? error);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function v2SessionState(states, sessionId) {
|
|
770
|
+
const key = sessionId || '__unknown__';
|
|
771
|
+
let state = states.get(key);
|
|
772
|
+
if (!state) {
|
|
773
|
+
state = { pendingText: '', lastDeliveredKey: '' };
|
|
774
|
+
states.set(key, state);
|
|
775
|
+
}
|
|
776
|
+
return state;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function v2FlushAssistant(state, directory, sessionId) {
|
|
780
|
+
const text = state.pendingText.trim();
|
|
781
|
+
if (!text || text === state.lastDeliveredKey) return;
|
|
782
|
+
v2RunHook({ hook_event_name: 'message.updated', ai_response: text }, directory, sessionId);
|
|
783
|
+
state.lastDeliveredKey = text;
|
|
784
|
+
state.pendingText = '';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function v2FilePath(file) {
|
|
788
|
+
if (typeof file === 'string') return file;
|
|
789
|
+
if (!file || typeof file !== 'object') return '';
|
|
790
|
+
return file.path ?? file.file ?? file.name ?? '';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function v2HandleEvent(event, directory, states) {
|
|
794
|
+
if (!event || typeof event !== 'object') return;
|
|
795
|
+
const type = typeof event.type === 'string' ? event.type : '';
|
|
796
|
+
const data = v2EventData(event);
|
|
797
|
+
const sessionId = v2SessionId(event);
|
|
798
|
+
const state = v2SessionState(states, sessionId);
|
|
799
|
+
const run = (payload) => v2RunHook(payload, directory, sessionId);
|
|
800
|
+
|
|
801
|
+
switch (type) {
|
|
802
|
+
case 'session.created':
|
|
803
|
+
run({ hook_event_name: 'session.created' });
|
|
804
|
+
break;
|
|
805
|
+
case 'session.text.delta':
|
|
806
|
+
state.pendingText += typeof data.delta === 'string' ? data.delta : (typeof data.text === 'string' ? data.text : '');
|
|
807
|
+
break;
|
|
808
|
+
case 'session.message.updated':
|
|
809
|
+
case 'message.updated':
|
|
810
|
+
if (data.role === 'assistant' && typeof data.text === 'string') state.pendingText = data.text;
|
|
811
|
+
break;
|
|
812
|
+
case 'session.step.ended': {
|
|
813
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
814
|
+
for (const file of files) {
|
|
815
|
+
const filePath = v2FilePath(file);
|
|
816
|
+
if (filePath) run({ hook_event_name: 'file.edited', file_path: filePath });
|
|
817
|
+
}
|
|
818
|
+
if (typeof data.text === 'string') state.pendingText += data.text;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
case 'session.idle':
|
|
822
|
+
v2FlushAssistant(state, directory, sessionId);
|
|
823
|
+
run({ hook_event_name: 'session.idle' });
|
|
824
|
+
break;
|
|
825
|
+
case 'session.compacted':
|
|
826
|
+
run({ hook_event_name: 'session.compacted' });
|
|
827
|
+
break;
|
|
828
|
+
default:
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function setup(ctx) {
|
|
834
|
+
const directory = ctx?.location?.directory || ctx?.location?.worktree || process.cwd();
|
|
835
|
+
const controller = new AbortController();
|
|
836
|
+
const states = new Map();
|
|
837
|
+
|
|
838
|
+
if (ctx?.tool?.hook) {
|
|
839
|
+
try {
|
|
840
|
+
Promise.resolve(ctx.tool.hook('execute.after', async (event) => {
|
|
841
|
+
const sessionId = v2SessionId(event);
|
|
842
|
+
const data = event && typeof event.input === 'object' ? event.input : {};
|
|
843
|
+
const tool = event?.tool ?? event?.name ?? '';
|
|
844
|
+
v2RunHook({
|
|
845
|
+
hook_event_name: 'tool.execute.after',
|
|
846
|
+
tool_name: tool,
|
|
847
|
+
tool_input: data,
|
|
848
|
+
}, directory, sessionId);
|
|
849
|
+
if (tool === 'shell') {
|
|
850
|
+
v2RunHook({
|
|
851
|
+
hook_event_name: 'command.executed',
|
|
852
|
+
command: data.command ?? data.cmd ?? '',
|
|
853
|
+
}, directory, sessionId);
|
|
854
|
+
}
|
|
855
|
+
})).catch((error) => v2ReportHookFailure('execute.after', error?.message ?? error));
|
|
856
|
+
} catch (error) {
|
|
857
|
+
v2ReportHookFailure('execute.after', error?.message ?? error);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (ctx?.event?.subscribe) {
|
|
862
|
+
void (async () => {
|
|
863
|
+
try {
|
|
864
|
+
const stream = ctx.event.subscribe({ signal: controller.signal });
|
|
865
|
+
for await (const event of stream) v2HandleEvent(event, directory, states);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (!controller.signal.aborted) v2ReportHookFailure('event.subscribe', error?.message ?? error);
|
|
868
|
+
}
|
|
869
|
+
})();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return () => controller.abort();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export default { id: 'memorix', setup };
|
|
719
876
|
`;
|
|
720
877
|
}
|
|
721
878
|
|
package/src/llm/provider.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from '../config.js';
|
|
21
21
|
|
|
22
22
|
export interface LLMConfig {
|
|
23
|
-
provider: 'openai' | 'anthropic' | 'openrouter' | 'atlascloud' | 'custom';
|
|
23
|
+
provider: 'openai' | 'anthropic' | 'openrouter' | 'requesty' | 'atlascloud' | 'custom';
|
|
24
24
|
apiKey: string;
|
|
25
25
|
model?: string;
|
|
26
26
|
baseUrl?: string;
|
|
@@ -201,6 +201,7 @@ const PROVIDER_DEFAULTS: Record<string, { baseUrl: string; model: string }> = {
|
|
|
201
201
|
openai: { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1-nano' },
|
|
202
202
|
anthropic: { baseUrl: 'https://api.anthropic.com/v1', model: 'claude-3-5-haiku-latest' },
|
|
203
203
|
openrouter: { baseUrl: 'https://openrouter.ai/api/v1', model: 'openai/gpt-4.1-nano' },
|
|
204
|
+
requesty: { baseUrl: 'https://router.requesty.ai/v1', model: 'openai/gpt-4.1-nano' },
|
|
204
205
|
atlascloud: { baseUrl: 'https://api.atlascloud.ai/v1', model: 'deepseek-ai/deepseek-v3.2' },
|
|
205
206
|
custom: { baseUrl: 'http://localhost:11434/v1', model: 'llama3' },
|
|
206
207
|
};
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from './image-payload.js';
|
|
20
20
|
|
|
21
21
|
// Providers that use the OpenAI-compatible /chat/completions Vision endpoint
|
|
22
|
-
const OPENAI_COMPATIBLE_PROVIDERS = new Set(['openai', 'openrouter', 'custom']);
|
|
22
|
+
const OPENAI_COMPATIBLE_PROVIDERS = new Set(['openai', 'openrouter', 'requesty', 'custom']);
|
|
23
23
|
const MAX_VISION_PROMPT_CHARS = 12_000;
|
|
24
24
|
const MAX_VISION_DESCRIPTION_CHARS = 12_000;
|
|
25
25
|
const MAX_VISION_LABELS = 50;
|
|
@@ -137,7 +137,7 @@ export async function analyzeImage(input: ImageInput): Promise<ImageAnalysisResu
|
|
|
137
137
|
const config = getLLMConfig()!;
|
|
138
138
|
if (!OPENAI_COMPATIBLE_PROVIDERS.has(config.provider)) {
|
|
139
139
|
throw new Error(
|
|
140
|
-
`Image analysis requires an OpenAI-compatible provider (openai, openrouter, or custom). ` +
|
|
140
|
+
`Image analysis requires an OpenAI-compatible provider (openai, openrouter, requesty, or custom). ` +
|
|
141
141
|
`Current provider "${config.provider}" uses a different API shape. ` +
|
|
142
142
|
`Set MEMORIX_LLM_PROVIDER=openai or configure an OpenAI-compatible base URL.`,
|
|
143
143
|
);
|