feat: model providers — 17 providers, dynamic model map, prompter#722
feat: model providers — 17 providers, dynamic model map, prompter#722Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Conversation
…ter refactor - src/models/_model_map.js: dynamic provider discovery, string-based auto-routing - New providers: grok.js, cerebras.js, mercury.js, novita.js, hyperbolic.js, glhf.js - Updated: gemini.js, claude.js, gpt.js, ollama.js, openrouter.js, qwen.js - Updated: prompter.js, deepseek.js, vllm.js, huggingface.js - Providers support streamed and non-streamed responses, embeddings where available - All providers load API keys via env-first keys.js
There was a problem hiding this comment.
Pull request overview
This PR expands the model-provider layer (now targeting 17 providers) by adding a dynamic provider/model routing map and refactoring Prompter to initialize models through that registry, with additional usage-tracking hooks and provider consistency updates.
Changes:
- Introduces dynamic model-string → provider adapter routing via
src/models/_model_map.js. - Refactors
src/models/prompter.jsto use the model map (and adds new prompt placeholder expansions + logging/usage hooks). - Updates multiple provider adapters to align with the new initialization/usage patterns (including usage capture in several adapters).
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/models/_model_map.js | Adds a guard for missing model configuration during provider selection. |
| src/models/prompter.js | Refactors model initialization to use model map; adds wiki/learnings placeholders; introduces usage tracking hooks and async profile save. |
| src/models/vllm.js | Refactors vLLM config/init; adds retries, <think> stripping, usage capture, and vision request wrapper. |
| src/models/ollama.js | Switches transport to node http/https; adds token usage capture; changes vision request formatting. |
| src/models/gemini.js | Updates Google GenAI request shape and captures usage; makes embedding return compatible across response shapes. |
| src/models/grok.js | Captures usage and hardens separator replacement against null responses. |
| src/models/gpt.js | Renames an internal messages variable (currently leaving unused/dead code). |
| src/models/cerebras.js | Adjusts unused parameter naming and embed signature. |
| src/models/openrouter.js | Removes unused key-check import; marks unused embed parameter. |
| src/models/qwen.js | Removes unused key-check import. |
| src/models/deepseek.js | Removes unused key-check import; marks unused embed parameter. |
| src/models/mercury.js | Removes unused key-check import. |
| src/models/claude.js | Marks unused embed parameter. |
| src/models/huggingface.js | Marks unused embed parameter. |
| src/models/novita.js | Marks unused embed parameter. |
| src/models/hyperbolic.js | Normalizes formatting; marks unused embed parameter. |
| src/models/glhf.js | Normalizes formatting; marks unused embed parameter. |
Comments suppressed due to low confidence (2)
src/models/ollama.js:57
resis assumed to be a string, butapiResponse['message']['content']can beundefined(orthis.send(...)can returnnullon error), which will makeres.includes(...)throw. Coerceresto a string before checking<think>tags (or add a type guard) so the retry/fallback logic works reliably.
});
if (apiResponse) {
res = apiResponse['message']['content'];
this._lastUsage = {
prompt_tokens: apiResponse.prompt_eval_count || 0,
completion_tokens: apiResponse.eval_count || 0,
total_tokens: (apiResponse.prompt_eval_count || 0) + (apiResponse.eval_count || 0),
};
} else {
res = 'No response data.';
this._lastUsage = null;
}
} catch (err) {
if (err.message.toLowerCase().includes('context length') && turns.length > 1) {
console.log('Context length exceeded, trying again with shorter context.');
return await this.sendRequest(turns.slice(1), systemMessage);
} else {
console.log(err);
res = 'My brain disconnected, try again.';
}
}
const hasOpenTag = res.includes("<think>");
const hasCloseTag = res.includes("</think>");
src/models/gpt.js:29
_messagesis built fromturnsand hasstop_seqappended, but it is never used (both branches later useturns/a newmessagesvariable). This is dead code and makes it unclear which message set is intended to be sent; either use_messagesin the responses-path or remove this block.
async sendRequest(turns, systemMessage, stop_seq='***') {
let _messages = strictFormat(turns);
_messages = _messages.map(message => {
message.content += stop_seq;
return message;
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| else if (typeof chat_model_profile !== 'undefined') { | ||
| this.embedding_model = createModel({api: chat_model_profile.api}); | ||
| } |
There was a problem hiding this comment.
chat_model_profile is declared inside the else block, but later referenced when selecting a default embedding provider. Because it's block-scoped, the typeof chat_model_profile !== 'undefined' branch will never be taken, so embeddings will always fall back to api: 'google'. Declare chat_model_profile in the outer scope and assign it in the non-ensemble path (and decide what the embedding default should be for the ensemble path).
| else if (typeof chat_model_profile !== 'undefined') { | |
| this.embedding_model = createModel({api: chat_model_profile.api}); | |
| } |
| // for backwards compatibility, move max_tokens to params | ||
| let max_tokens = null; | ||
| let _max_tokens = null; | ||
| if (this.profile.max_tokens) | ||
| max_tokens = this.profile.max_tokens; | ||
| _max_tokens = this.profile.max_tokens; | ||
|
|
There was a problem hiding this comment.
The comment says max_tokens is moved into params for backwards compatibility, but _max_tokens is never used and this.profile.max_tokens is never applied to profile.params passed into createModel(). Either remove this dead code or actually merge max_tokens into this.profile.params (and consider deleting this.profile.max_tokens afterward to avoid ambiguity).
| // Handle <think> tags (Gemma-3 / reasoning models may produce these) | ||
| const hasOpenTag = res.includes('<think>'); | ||
| const hasCloseTag = res.includes('</think>'); | ||
|
|
There was a problem hiding this comment.
res can be null/undefined (e.g., if completion.choices[0].message.content is missing) but the code calls res.includes(...), which will throw a TypeError and skip the retry/return logic. Coerce to a string before checking tags (e.g., res = res ?? '') or guard with typeof res === 'string'.
| import settings from '../agent/settings.js'; | ||
| import { deepSanitize } from '../../settings.js'; | ||
| import { promises as fs } from 'fs'; |
There was a problem hiding this comment.
deepSanitize is imported as a named export from ../../settings.js, but settings.js only exports a default settings object (no deepSanitize). This will throw at module load time. Either export deepSanitize from settings.js, or move the sanitize helper into this module (or another util) and import it from there.
| import { EnsembleModel } from '../ensemble/controller.js'; | ||
| import { UsageTracker } from '../utils/usage_tracker.js'; |
There was a problem hiding this comment.
Imports for EnsembleModel and UsageTracker point to modules that don't exist in this repo (src/ensemble/controller.js and src/utils/usage_tracker.js). This will fail at runtime when loading Prompter. Add these modules in the PR, or correct the import paths to the existing implementations.
| import { EnsembleModel } from '../ensemble/controller.js'; | |
| import { UsageTracker } from '../utils/usage_tracker.js'; |
All of those providers already existed, GLHF has been rebranded though, it's not HF API though. |
Model Provider Updates
Adds 6 new providers and updates all existing ones. Replaces hard-coded model routing with a dynamic model-string auto-routing system.
New Providers
ovita.js\ — Novita AI
Core Changes
Updated Providers
gemini, claude, gpt, ollama, openrouter, qwen, deepseek, vllm, huggingface