Files
autoreg-skills-max/skills/chat2api-proxy/references/function-calling-pattern.md

5.0 KiB

Function Calling via Prompt Engineering — Pattern

Problem

ChatGPT Web (and most chat2api projects) don't support native function calling / tool use. The /backend-api/conversation endpoint doesn't accept tools or tool_choice parameters.

Solution

Inject tool definitions into the system prompt and parse JSON tool calls from the model's response.

Implementation

1. Tool System Prompt Template

TOOL_CALL_SYSTEM_PROMPT = """# Tools

You have access to the following tools:

{tools_json}

## Tool Use Protocol

When you need to use a tool, respond ONLY with a JSON object in the following format, wrapped in ```json code blocks:

```json
{{
  "tool_calls": [
    {{
      "id": "call_<random>",
      "type": "function",
      "function": {{
        "name": "<tool_name>",
        "arguments": "<json_encoded_arguments>"
      }}
    }}
  ]
}}

After the tool call, the user will respond with the tool output. Then you can continue the conversation.

IMPORTANT:

  • Only call tools when absolutely necessary
  • Use proper JSON encoding for arguments
  • If no tool is needed, respond normally without JSON
  • Do NOT wrap your normal responses in JSON — only tool calls """

### 2. Inject into Messages

```python
def inject_tools_into_messages(messages: list, tools: list | None) -> list:
    if not tools:
        return messages
    
    tools_json = json.dumps(tools, indent=2, ensure_ascii=False)
    tool_system_msg = {
        "role": "system",
        "content": TOOL_CALL_SYSTEM_PROMPT.format(tools_json=tools_json)
    }
    
    # Find existing system message or prepend
    for msg in messages:
        if msg.get("role") == "system":
            msg["content"] += "\n\n" + tool_system_msg["content"]
            return messages
    
    messages.insert(0, tool_system_msg)
    return messages

3. Parse Tool Calls from Response

def parse_tool_calls_from_response(content: str) -> tuple[list | None, str | None]:
    """Returns (tool_calls, text_content) or (None, content)"""
    # Look for ```json ... ``` blocks
    json_pattern = r'```json\s*\n(.*?)\n\s*```'
    json_match = re.search(json_pattern, content, re.DOTALL)
    
    if json_match:
        try:
            parsed = json.loads(json_match.group(1))
            if isinstance(parsed, dict) and "tool_calls" in parsed:
                # Extract text before/after JSON
                text_before = content[:json_match.start()].strip()
                text_after = content[json_match.end():].strip()
                return parsed["tool_calls"], (text_before + " " + text_after).strip() or None
        except json.JSONDecodeError:
            pass
    
    return None, content

4. Build OpenAI-Compatible Response

def build_openai_tool_call_response(model, tool_calls, text_content=None, finish_reason="tool_calls"):
    return {
        "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
        "object": "chat.completion",
        "created": int(time.time()),
        "model": model,
        "choices": [{
            "index": 0,
            "message": {
                "role": "assistant",
                "content": text_content,
                "tool_calls": [
                    {
                        "id": tc.get("id", f"call_{uuid.uuid4().hex[:24]}"),
                        "type": "function",
                        "function": {
                            "name": tc["function"]["name"],
                            "arguments": tc["function"]["arguments"]
                            if isinstance(tc["function"]["arguments"], str)
                            else json.dumps(tc["function"]["arguments"])
                        }
                    }
                    for tc in tool_calls
                ]
            },
            "finish_reason": finish_reason
        }],
        "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
    }

5. Integration in ChatService

In set_dynamic_data():

self.tools = self.data.get("tools", None)
if self.tools:
    self.api_messages = inject_tools_into_messages(list(self.api_messages), self.tools)

In send_conversation() — after getting response, before returning:

if self.tools:
    full_content = collect_all_content(response)
    tool_calls, text = parse_tool_calls_from_response(full_content)
    if tool_calls:
        return build_openai_tool_call_response(self.resp_model, tool_calls, text)

Limitations

  • Prompt engineering, not native: The model may ignore tool instructions or hallucinate tool calls
  • No streaming tool calls: Tool calls are detected only after full response
  • JSON parsing fragility: Models may produce malformed JSON
  • Token waste: System prompt with tools consumes tokens from the chat window
  • No parallel tool calls: The pattern supports single tool_call per response

Alternatives

For better tool calling support, use a project with native function calling like xiaoY233/Chat2API which has built-in prompt-engineering function calling with model mapping and context management.