Tool calling
Define a function, execute the requested tool, and return its result for the final answer.
The model returns a tool name and arguments. Your application executes the tool and sends its result back. A tool call is not the final answer.
This example reads local demonstration data: Paris is always 22°C. It does not call a weather service. GET /openai/v1/models lists tools and tool_choice in deepseek-flash's supported_parameters. The two-turn flow below was verified on 2026-10-12.
1. Prepare your terminal
Use a Praxis cloud API key in PRAXIS_API_KEY; see API keys and authentication. Do not put the key in source code. Install Python and uv; TypeScript examples use Bun.
test -n "$PRAXIS_API_KEY"Expected: exit code 0. The examples default to https://api.deepraxis.com. To use another environment, set PRAXIS_API_BASE to its API origin, without a trailing slash or API path.
Save each Python block using its indicated filename. Run it with uv run -q --no-project --with openai --with anthropic python <filename>. Each block is independent.
2. Define a tool and complete both turns
Save as tool-calling.py. Run uv run -q --no-project --with openai --with anthropic python tool-calling.py.
Define the name, description, and parameters of get_temperature, then request a call. Validate arguments, run the local lookup, and return the result with its original tool_call_id. Finally, request a text answer. Both turns set reasoning_effort="none".
import json
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/openai/v1')
tools = [{'type':'function','function':{
'name':'get_temperature', 'description':'Read the demo temperature for Paris.',
'parameters':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
'required':['city'],'additionalProperties':False}}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
first = client.chat.completions.create(model='deepseek-flash', messages=messages,
tools=tools, reasoning_effort='none', tool_choice={'type':'function','function':{'name':'get_temperature'}}, max_tokens=1024)
print('X-Request-Id:', first._request_id)
message = first.choices[0].message
assert first.choices[0].finish_reason == 'tool_calls' and message.tool_calls
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
assert call.function.name == 'get_temperature'
args = json.loads(call.function.arguments)
assert args == {'city':'Paris'}, args
result = {'city':args['city'], 'temperature_c':22}
messages.append({'role':'tool','tool_call_id':call.id,'content':json.dumps(result)})
second = client.chat.completions.create(model='deepseek-flash', messages=messages,
tools=tools, reasoning_effort='none', tool_choice='none', max_tokens=1024)
print('X-Request-Id:', second._request_id)
answer = second.choices[0].message.content or ''
assert second.choices[0].finish_reason == 'stop' and '22' in answer, answer
print(answer)Expected: the first turn returns finish_reason=tool_calls and arguments {"city":"Paris"}. The second answer contains 22 with finish_reason=stop. Exit code: 0.
Preserve the complete first assistant message. Return one role="tool" message per call. Do not invent call IDs or send results as ordinary user text. Arguments are JSON strings: parse and validate them before execution. Never execute arbitrary model-provided function names. This example ends the tool phase with tool_choice="none". Applications using auto should handle repeated tool turns with a maximum turn limit.
3. Run the same flow with TypeScript
Save as tool-calling.ts. Run bun tool-calling.ts. It uses built-in fetch; no SDK installation is needed.
import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.com';
async function chat(body: object) {
const response = await fetch(`${base}/openai/v1/chat/completions`, {
method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({model: 'deepseek-flash', max_tokens: 1024, reasoning_effort: 'none', ...body}),
});
console.log('X-Request-Id:', response.headers.get('x-request-id'));
const data = await response.json();
assert(response.ok, JSON.stringify(data));
return data;
}
const tools = [{type: 'function', function: {name: 'get_temperature',
description: 'Read the demo temperature for Paris.',
parameters: {type: 'object', properties: {city: {type: 'string', enum: ['Paris']}},
required: ['city'], additionalProperties: false}}}];
const messages: any[] = [{role: 'user', content: 'Use get_temperature for Paris, then tell me the temperature in Celsius.'}];
const first = await chat({messages, tools, tool_choice: {type: 'function', function: {name: 'get_temperature'}}});
const choice = first.choices[0];
assert.equal(choice.finish_reason, 'tool_calls');
assert(choice.message.tool_calls?.length);
messages.push(choice.message);
for (const call of choice.message.tool_calls) {
assert.equal(call.function.name, 'get_temperature');
const args = JSON.parse(call.function.arguments);
assert.deepEqual(args, {city: 'Paris'});
messages.push({role: 'tool', tool_call_id: call.id,
content: JSON.stringify({city: args.city, temperature_c: 22})});
}
const second = await chat({messages, tools, tool_choice: 'none'});
assert.equal(second.choices[0].finish_reason, 'stop');
assert(second.choices[0].message.content.includes('22'));
console.log(second.choices[0].message.content);Expected: the same tool call and result submission, followed by an answer containing 22. Exit code: 0.
4. Use Anthropic Messages
Messages defines parameters with input_schema. It returns tool_use content blocks and stop_reason="tool_use". Preserve the complete assistant content, then send tool_result in the next role="user" message. Match the original call using tool_use_id. This example was verified with claude-opus-5.
Save as tool-calling-anthropic.py. Run uv run -q --no-project --with openai --with anthropic python tool-calling-anthropic.py.
import json
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.com') + '/anthropic')
tools = [{'name':'get_temperature','description':'Read the demo temperature for Paris.',
'input_schema':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
'required':['city'],'additionalProperties':False}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
tools=tools, tool_choice={'type':'tool','name':'get_temperature'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
first = raw.parse()
assert first.stop_reason == 'tool_use'
messages.append({'role':'assistant','content':[block.model_dump(exclude_none=True) for block in first.content]})
results = []
for block in first.content:
if block.type == 'tool_use':
assert block.name == 'get_temperature' and block.input == {'city':'Paris'}
results.append({'type':'tool_result','tool_use_id':block.id,
'content':json.dumps({'city':'Paris','temperature_c':22})})
assert results
messages.append({'role':'user','content':results})
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
tools=tools, tool_choice={'type':'none'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
second = raw.parse()
answer = ''.join(block.text for block in second.content if block.type == 'text')
assert second.stop_reason == 'end_turn' and '22' in answer, answer
print(answer)Expected: a tool request, then an answer containing 22 with stop_reason=end_turn. Exit code: 0.
Observed failures and fixes
Forcing a named function without disabling thinking returned HTTP 400 from deepseek-flash:
tool_choice a named function is not supported while thinking is enabled on this model. Set reasoning effort to "none" or use tool_choice "auto".
The verified fix used here is explicit reasoning_effort="none". Setting it only on the first turn also produced HTTP 400 on the second turn:
The selected thinking setting is not supported on this model route.
Keeping reasoning_effort="none" on both turns passed. Preserve this parameter when retrying.