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.

Protocol details:OpenAI · Anthropic。

API reference → · Cookbook →

Export

On this page