# Tool calling (/en/docs/developers/model/guides/tool-calling)



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 [#1-prepare-your-terminal]

Use a Praxis cloud API key in `PRAXIS_API_KEY`; see [API keys and authentication](/en/docs/developers/account/api-keys). Do not put the key in source code. Install Python and [uv](https://docs.astral.sh/uv/getting-started/installation/); TypeScript examples use [Bun](https://bun.sh/docs/installation).

```bash
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 [#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"`.

```python
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 [#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.

```typescript
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 [#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`.

```python
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 [#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](https://developers.openai.com/api/docs/guides/function-calling) · [Anthropic](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)。

[API reference →](/en/docs/api/compatible/openai) · [Cookbook →](/en/docs/cookbook)
