Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ const result = await ctx.text2run(`
models({
// Set default model when no @model specified
default: "gpt-4.1-mini",

// Or set the default model and its request defaults together.
// All fields except `model` are added to its outgoing request payload.
default: {
model: "openai/gpt-5.6-terra",
reasoning: { max_tokens: 8000 }
},

// Create aliases for models
alias: { "gpt": "gpt-4.1-mini" },
Expand All @@ -117,8 +124,23 @@ models({
openai: process.env.OPENAI_KEY,
anthropic: process.env.ANTHROPIC_KEY,
ollama: process.env.OLLAMA_URL
},

// Provider request fields used as defaults for individual models.
// Keys are real model IDs, so aliases and `default` inherit them.
params: {
"openai/gpt-5.6-luna": {
reasoning: { max_tokens: 8000 }
},
"claude-sonnet-4-20250514": {
thinking: { type: "enabled", budget_tokens: 8000 },
max_tokens: 12000
}
}
})

// `params` are deep-merged into the outgoing request. Fields set later in a
// chat (for example by `| prop`) override these configured defaults.
```

### Provider-Specific
Expand Down
51 changes: 45 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,47 @@ const tune = require("tune-sdk")
const man = require("tune-sdk/man");
man.addPackage(__dirname)

// Merge request defaults without replacing nested provider options. Values in
// `override` win, so inline model processors can override configured defaults.
function mergeParams(defaults = {}, override = {}) {
const result = { ...defaults }
for (const [key, value] of Object.entries(override)) {
result[key] = value && typeof value === "object" && !Array.isArray(value)
? mergeParams(defaults[key] || {}, value)
: value
}
return result
}

function withParams(node, defaults) {
if (!node || !defaults || node.type !== "llm") return node
return {
...node,
exec: (payload, ctx) => node.exec(mergeParams(defaults, payload), ctx)
}
}

// `default` may be a model name or `{ model, ...requestDefaults }`.
function defaultConfig(value) {
if (typeof value === "string") return { model: value, params: {} }
if (!value || typeof value !== "object") return { model: undefined, params: {} }
const { model, ...params } = value
return { model, params }
}

function createModelsMiddleware(options = {}) {
const {
cache = true, // disk cache by default for text editors
cacheTtl = 3600000, // 1 hour default
default: defaultModel,
default: defaultOption,
apiKeys = {},
expose = undefined,
alias = {}
alias = {},
params = {}
} = options;

const { model: defaultModel, params: defaultParams } = defaultConfig(defaultOption)

// Create configured providers
const providers = [
ollama({ cache: false, apiKey: apiKeys.ollama }),
Expand All @@ -32,22 +63,27 @@ function createModelsMiddleware(options = {}) {
openrouter({ cache, cacheTtl, apiKey: apiKeys.openrouter }),
];

async function resolveModel(context, modelName, args, requestDefaults = {}) {
const node = await tune.resolve(context, modelName, args, providers)
return withParams(node, mergeParams(params[modelName], requestDefaults))
}

return async function models(name, args) {
// Handle default model resolution
if (name === "default" && args.type === "llm" && defaultModel) {
return tune.resolve(this, defaultModel, args, providers);
return resolveModel(this, defaultModel, args, defaultParams)
}


// Handle aliases
// Resolve aliases before looking up request defaults. This makes aliases
// and @default share the configuration of their real model ID.
const resolvedName = alias[name] || name;

// TODO: what if name is regex?
if (expose && expose.indexOf(resolvedName) === -1) {
return
}

return tune.resolve(this, resolvedName, args, providers);
return resolveModel(this, resolvedName, args)
}
}

Expand All @@ -60,4 +96,7 @@ module.exports.gemini = gemini;
module.exports.mistral = mistral;
module.exports.groq = groq;
module.exports.ollama = ollama;
module.exports.mergeParams = mergeParams;
module.exports.withParams = withParams;
module.exports.defaultConfig = defaultConfig;

43 changes: 42 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const fs = require('fs');
const path = require('path');
const tune = require('tune-sdk');
const models = require('../src/index');
const { openai, anthropic, groq, mistral, gemini, openrouter } = require('../src/index');
const { openai, anthropic, groq, mistral, gemini, openrouter, mergeParams, withParams, defaultConfig } = require('../src/index');
const llmUtils = require('../src/llm-utils.js')

require('dotenv').config()
Expand All @@ -22,6 +22,47 @@ const env = {

const tests = {};


tests.params_merge = async function() {
const defaults = {
reasoning: { max_tokens: 8000, enabled: true },
max_tokens: 12000,
messages: [{ role: "system", content: "default" }]
}
const payload = {
reasoning: { max_tokens: 2000 },
max_tokens: 16000,
messages: [{ role: "user", content: "call" }]
}
const merged = mergeParams(defaults, payload)
assert.deepEqual(merged.reasoning, { max_tokens: 2000, enabled: true })
assert.equal(merged.max_tokens, 16000)
assert.deepEqual(merged.messages, payload.messages)
assert.deepEqual(defaults.reasoning, { max_tokens: 8000, enabled: true })

let received
const node = withParams({
type: "llm",
exec: async (request) => {
received = request
return "request created"
}
}, defaults)
assert.equal(await node.exec(payload), "request created")
assert.deepEqual(received, merged)

assert.deepEqual(defaultConfig("gpt-4.1-mini"), {
model: "gpt-4.1-mini", params: {}
})
assert.deepEqual(defaultConfig({
model: "openai/gpt-5.6-terra",
reasoning: { max_tokens: 8000 }
}), {
model: "openai/gpt-5.6-terra",
params: { reasoning: { max_tokens: 8000 } }
})
}

tests.api_keys = async function(){
assert.ok(process.env.OPENAI_KEY, "OPENAI_KEY has to be set for testing")
assert.ok(process.env.ANTHROPIC_KEY, "ANTHROPIC_KEY has to be set for testing")
Expand Down