# Current Message Schema Analysis

## Overview

The Kollabor CLI uses an adapter-based architecture to communicate with LLM APIs, supporting both OpenAI and Anthropic formats. The system handles requests through `APICommunicationService` which delegates provider-specific formatting to adapter classes.

**Key Files:**
- `kollabor/llm/api_communication_service.py` - Main API communication layer
- `kollabor/llm/api_adapters/base.py` - Base adapter interface
- `kollabor/llm/api_adapters/openai_adapter.py` - OpenAI format implementation
- `kollabor/llm/api_adapters/anthropic_adapter.py` - Anthropic format implementation
- `kollabor/models/base.py` - Core data models
- `kollabor/llm/conversation_manager.py` - Conversation state management

---

## Request Structure

### OpenAI Format Request

**Endpoint:** `POST {base_url}/v1/chat/completions`

**Request Schema:**

```python
{
    "model": str,                    # Required: Model identifier (e.g., "gpt-4")
    "messages": [                    # Required: Conversation messages
        {
            "role": str,             # "system" | "user" | "assistant" | "tool"
            "content": str,          # Message content
            # Optional for assistant messages with tool calls:
            "tool_calls": [
                {
                    "id": str,       # Tool call ID
                    "type": "function",
                    "function": {
                        "name": str,     # Function name
                        "arguments": str # JSON string of arguments
                    }
                }
            ]
            # Optional for tool messages:
            "tool_call_id": str      # References assistant's tool call ID
        }
    ],
    "temperature": float,            # Optional: Sampling temperature
    "max_tokens": int,               # Optional: Max completion tokens
    "stream": bool,                  # Optional: Enable streaming
    "tools": [                       # Optional: Tool definitions
        {
            "type": "function",
            "function": {
                "name": str,         # Function name
                "description": str,  # Function description
                "parameters": {      # JSON Schema for arguments
                    "type": "object",
                    "properties": {...},
                    "required": [...]
                }
            }
        }
    ],
    "tool_choice": str | dict        # Optional: "auto" | "none" | {"type": "function", "name": "..."}
}
```

**Headers:**
```python
{
    "Content-Type": "application/json",
    "Authorization": "Bearer {api_token}"
}
```

### Anthropic Format Request

**Endpoint:** `POST {base_url}/v1/messages`

**Request Schema:**

```python
{
    "model": str,                    # Required: Model identifier
    "max_tokens": int,               # Required: Max completion tokens
    "messages": [                    # Required: Conversation messages (no system messages)
        {
            "role": str,             # "user" | "assistant"
            "content": [             # Array of content blocks
                {
                    "type": "text",
                    "text": str
                },
                # Tool use blocks in assistant messages:
                {
                    "type": "tool_use",
                    "id": str,       # Unique tool use ID
                    "name": str,     # Tool name
                    "input": {...}   # Tool arguments (object)
                },
                # Tool result blocks in user messages:
                {
                    "type": "tool_result",
                    "tool_use_id": str,  # References assistant's tool_use ID
                    "content": str,
                    "is_error": bool
                }
            ]
        }
    ],
    "system": str,                   # Optional: System prompt (separate from messages)
    "temperature": float,            # Optional: Sampling temperature
    "stream": bool,                  # Optional: Enable streaming
    "tools": [                       # Optional: Tool definitions
        {
            "name": str,             # Tool name
            "description": str,      # Tool description
            "input_schema": {        # JSON Schema for arguments
                "type": "object",
                "properties": {...},
                "required": [...]
            }
        }
    ],
    "tool_choice": dict | None       # Optional: {"type": "auto"} | {"type": "any"} | {"type": "tool", "name": "..."}
}
```

**Headers:**
```python
{
    "Content-Type": "application/json",
    "x-api-key": "{api_token}",      # Note: Different from OpenAI!
    "anthropic-version": "2023-06-01"
}
```

---

## Response Structure

### OpenAI Format Response

**Non-Streaming Response Schema:**

```python
{
    "id": str,                       # Response ID
    "object": "chat.completion",
    "created": int,                  # Unix timestamp
    "model": str,                    # Model used
    "choices": [
        {
            "index": int,
            "message": {
                "role": "assistant",
                "content": str,      # Response text (empty if tool_calls present)
                "tool_calls": [      # Optional: Tool calls requested
                    {
                        "id": str,   # Tool call ID
                        "type": "function",
                        "function": {
                            "name": str,         # Function name
                            "arguments": str    # JSON string of arguments
                        }
                    }
                ]
            },
            "finish_reason": str     # "stop" | "tool_calls" | "length" | "content_filter"
        }
    ],
    "usage": {
        "prompt_tokens": int,
        "completion_tokens": int,
        "total_tokens": int
    }
}
```

**Streaming Response:**

Server-Sent Events (SSE) format:

```
data: {"choices":[{"delta":{"content":"chunk"}}]}

data: {"choices":[{"delta":{}}],"finish_reason":"stop"}

data: [DONE]
```

### Anthropic Format Response

**Non-Streaming Response Schema:**

```python
{
    "id": str,                       # Response ID
    "type": "message",
    "role": "assistant",
    "content": [                     # Array of content blocks
        {
            "type": "text",
            "text": str              # Response text
        },
        # Tool use blocks:
        {
            "type": "tool_use",
            "id": str,               # Unique tool use ID
            "name": str,             # Tool name
            "input": {...}           # Tool arguments (object)
        }
    ],
    "stop_reason": str,              # "end_turn" | "tool_use" | "max_tokens"
    "model": str,
    "usage": {
        "input_tokens": int,         # Note: Different naming from OpenAI
        "output_tokens": int
    }
}
```

**Streaming Response:**

Server-Sent Events (SSE) format similar to OpenAI but with different event types.

---

## Message Flow

### Request Flow

```
User Input
    ↓
ConversationManager.add_message()
    ↓ (creates ConversationMessage)
LLMService.send_message()
    ↓ (prepares conversation history)
APICommunicationService.call_llm()
    ↓ (delegates to adapter)
Adapter.format_request()
    ↓ (formats for OpenAI/Anthropic)
HTTP POST to API endpoint
```

**Internal Message Format (before API adapter):**

```python
# From ConversationMessage (kollabor/models/base.py)
{
    "role": str,                     # "user" | "assistant" | "system" | "tool"
    "content": str,                  # Message content
    "timestamp": datetime,
    "metadata": dict,
    "thinking": Optional[str]        # Thinking content (not sent to API)
}

# From conversation_history (list of dicts)
{
    "role": str,
    "content": str,
    # Optional for assistant messages with tool calls (OpenAI format):
    "tool_calls": [...],
    # Optional for assistant messages with tool uses (Anthropic format):
    "tool_uses": [
        {
            "id": str,
            "name": str,
            "input": dict
        }
    ]
}
```

**Transformation to API Format:**

1. **OpenAI Adapter** (`OpenAIAdapter._format_messages()`):
   - Converts internal `role`/`content` to OpenAI message format
   - Preserves `tool_calls` in assistant messages
   - Converts `tool` role messages with `tool_call_id`

2. **Anthropic Adapter** (`AnthropicAdapter._separate_system_message()`):
   - Extracts system messages to separate `system` field
   - Converts user messages to content blocks: `{"type": "text", "text": "..."}`
   - Converts assistant messages to content blocks (text + tool_use blocks)
   - Formats tool results as `tool_result` content blocks in user messages

### Response Flow

```
HTTP Response from API
    ↓
APICommunicationService._execute_request()
    ↓ (delegates to adapter)
Adapter.parse_response()
    ↓ (returns AdapterResponse)
APICommunicationService extracts:
    - content: str
    - tool_calls: List[ToolCallResult]
    - usage: Dict[str, int]
    - stop_reason: str
    ↓
LLMService processes response
    ↓
ResponseParser.parse_response()
    ↓ (extracts special tags: <thinking>, <terminal>, <tool>, file ops)
Display to user
```

**AdapterResponse (Unified Format):**

```python
@dataclass
class AdapterResponse:
    content: str                     # Text content from response
    tool_calls: List[ToolCallResult] # Tool calls requested by LLM
    usage: Dict[str, int]            # Token usage statistics
    stop_reason: str                 # Why response ended
    raw_response: Dict[str, Any]     # Original unmodified response
    model: str                       # Model that generated response

@dataclass
class ToolCallResult:
    tool_id: str                     # Unique identifier
    tool_name: str                   # Tool/function name
    arguments: Dict[str, Any]        # Tool arguments (parsed)
```

**Transformation from API Format:**

1. **OpenAI Adapter** (`OpenAIAdapter.parse_response()`):
   - Extracts content from `choices[0].message.content`
   - Parses `tool_calls` array, extracts function name and JSON arguments
   - Maps finish_reason: `"stop"` → `"end_turn"`, `"tool_calls"` → `"tool_use"`, `"length"` → `"max_tokens"`
   - Extracts usage: `prompt_tokens`, `completion_tokens`, `total_tokens`

2. **Anthropic Adapter** (`AnthropicAdapter.parse_response()`):
   - Extracts content from `content` array of blocks
   - Parses `tool_use` blocks, extracts id, name, input
   - Maps stop_reason directly (`"end_turn"`, `"tool_use"`, `"max_tokens"`)
   - Extracts usage: `input_tokens` → `prompt_tokens`, `output_tokens` → `completion_tokens`

---

## Type Definitions

### Core Models

**ConversationMessage** (`kollabor/models/base.py`):

```python
@dataclass
class ConversationMessage:
    role: str                        # "user" | "assistant" | "system" | "tool"
    content: str                     # Message content
    timestamp: datetime              # When message was created
    metadata: Dict[str, Any]         # Additional metadata
    thinking: Optional[str] = None   # Thinking process (not sent to API)
```

### Adapter Models

**ToolCallingFormat** (Enum):

```python
class ToolCallingFormat(Enum):
    OPENAI = "openai"                # /v1/chat/completions, parameters, tool_calls
    ANTHROPIC = "anthropic"          # /v1/messages, input_schema, tool_use
```

**ToolCallResult** (Unified):

```python
@dataclass
class ToolCallResult:
    tool_id: str                     # Unique identifier (OpenAI: id, Anthropic: tool_use_id)
    tool_name: str                   # Tool/function name
    arguments: Dict[str, Any]        # Parsed arguments (object, not JSON string)
```

**AdapterResponse** (Unified):

```python
@dataclass
class AdapterResponse:
    content: str                     # Text content from response
    tool_calls: List[ToolCallResult] # Tool calls requested by LLM
    usage: Dict[str, int]            # Token usage (prompt_tokens, completion_tokens, total_tokens)
    stop_reason: str                 # "end_turn" | "tool_use" | "max_tokens" | "unknown"
    raw_response: Dict[str, Any]     # Original API response
    model: str                       # Model identifier
```

### Tool Definitions

**Generic Tool Format** (Internal):

```python
{
    "name": str,                     # Tool/function name
    "description": str,              # What the tool does
    "parameters": {...} | "input_schema": {...}  # JSON Schema (interchangeable keys)
}
```

**OpenAI Tool Format**:

```python
{
    "type": "function",
    "function": {
        "name": str,
        "description": str,
        "parameters": {              # JSON Schema
            "type": "object",
            "properties": {...},
            "required": [...]
        }
    }
}
```

**Anthropic Tool Format**:

```python
{
    "name": str,
    "description": str,
    "input_schema": {                # JSON Schema
        "type": "object",
        "properties": {...},
        "required": [...]
    }
}
```

### Tool Result Messages

**OpenAI Format** (`OpenAIAdapter.format_tool_result()`):

```python
{
    "role": "tool",
    "tool_call_id": str,             # References assistant's tool call ID
    "content": str                   # Tool execution result (JSON stringified if not string)
}
```

**Anthropic Format** (`AnthropicAdapter.format_tool_result()`):

```python
{
    "role": "user",
    "content": [
        {
            "type": "tool_result",
            "tool_use_id": str,      # References assistant's tool_use ID
            "content": str,          # Tool execution result (JSON stringified if not string)
            "is_error": bool         # Whether result is an error
        }
    ]
}
```

---

## Integration Notes for OpenAI SDK

### What Needs to Change

#### 1. Request Preparation

**Current Approach:**
- Manually constructs request payloads in adapters
- Supports both OpenAI and Anthropic formats
- Tool definitions use generic format with `parameters` or `input_schema` keys

**OpenAI SDK Approach:**
- Use `openai.ChatCompletion` or `openai.chat.completions.create()`
- SDK handles request formatting automatically
- Tools defined using SDK's schema format

**Migration Path:**
```python
# Current (manual)
payload = {
    "model": "gpt-4",
    "messages": messages,
    "tools": formatted_tools
}
response = await session.post(url, json=payload)

# With OpenAI SDK
from openai import AsyncOpenAI
client = AsyncOpenAI(api_key=token)
response = await client.chat.completions.create(
    model="gpt-4",
    messages=messages,
    tools=tools  # SDK handles formatting
)
```

#### 2. Response Parsing

**Current Approach:**
- Adapters parse raw JSON responses
- Extract content, tool_calls, usage, stop_reason
- Convert to unified AdapterResponse format

**OpenAI SDK Approach:**
- SDK returns structured response objects
- Direct attribute access: `response.choices[0].message.content`
- Tool calls: `response.choices[0].message.tool_calls`

**Migration Path:**
```python
# Current (manual parsing)
parsed = adapter.parse_response(raw_json)
content = parsed.content
tool_calls = parsed.tool_calls

# With OpenAI SDK
response = await client.chat.completions.create(...)
content = response.choices[0].message.content
tool_calls = response.choices[0].message.tool_calls or []
```

#### 3. Tool Call Handling

**Current Approach:**
- Extracts tool calls from `tool_calls` array (OpenAI) or `content` blocks (Anthropic)
- Parses JSON arguments from string (OpenAI) or object (Anthropic)
- Returns unified `ToolCallResult` format

**OpenAI SDK Approach:**
- SDK returns tool calls as structured objects
- Arguments already parsed as Python dicts
- Direct access to tool call IDs and names

**Migration Path:**
```python
# Current
for tc in response.tool_calls:
    args = json.loads(tc.function.arguments)
    tool_calls.append(ToolCallResult(
        tool_id=tc.id,
        tool_name=tc.function.name,
        arguments=args
    ))

# With OpenAI SDK
for tc in response.choices[0].message.tool_calls or []:
    tool_calls.append(ToolCallResult(
        tool_id=tc.id,
        tool_name=tc.function.name,
        arguments=tc.function.arguments  # Already a dict!
    ))
```

#### 4. Streaming Support

**Current Approach:**
- Manual SSE parsing in `_handle_streaming_response()`
- Reads chunks, parses JSON, extracts `delta.content`
- Accumulates content chunks

**OpenAI SDK Approach:**
- SDK provides async iterator over streaming chunks
- Direct access to delta content
- Handles SSE parsing automatically

**Migration Path:**
```python
# Current (manual SSE parsing)
async for chunk in response.content.iter_chunked(1024):
    # Parse SSE, extract delta.content...

# With OpenAI SDK
stream = await client.chat.completions.create(..., stream=True)
async for chunk in stream:
    content = chunk.choices[0].delta.content or ""
    await streaming_callback(content)
```

### Adapter Strategy for OpenAI SDK

**Option 1: Create OpenAI SDK Adapter**

Create `OpenAI_SDK_Adapter` that wraps the OpenAI SDK:

```python
class OpenAI_SDK_Adapter(BaseAPIAdapter):
    def __init__(self, api_key: str, base_url: str = None):
        from openai import AsyncOpenAI
        self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)

    async def call_llm(self, messages, tools=None, **kwargs):
        response = await self.client.chat.completions.create(
            model=kwargs.get("model"),
            messages=messages,
            tools=tools,
            stream=kwargs.get("stream", False)
        )
        return self._parse_sdk_response(response)
```

**Option 2: Hybrid Approach**

Keep adapter interface but use SDK internally:

```python
class OpenAIAdapter(BaseAPIAdapter):
    def __init__(self, base_url: str, use_sdk: bool = True):
        super().__init__(base_url)
        if use_sdk:
            from openai import AsyncOpenAI
            self.client = AsyncOpenAI(base_url=base_url)
        else:
            self.client = None  # Use manual HTTP

    async def call_llm(self, messages, tools=None, **kwargs):
        if self.client:
            return await self._call_with_sdk(messages, tools, **kwargs)
        else:
            return await self._call_manual(messages, tools, **kwargs)
```

### Compatibility Considerations

#### Fields That Match

- **Model**: Both use string model identifiers
- **Messages**: Both use `role` + `content` structure (with differences in system message handling)
- **Tools**: Both use JSON Schema for parameters (different key names)
- **Temperature**: Both use float 0.0-2.0
- **Max Tokens**: Both use integers

#### Fields That Differ

| Aspect | OpenAI | Anthropic |
|--------|--------|-----------|
| **System Message** | In messages array | Separate `system` field |
| **Tool Schema Key** | `parameters` | `input_schema` |
| **Tool Calls** | `tool_calls` array | `content` blocks with `type: "tool_use"` |
| **Tool Results** | `role: "tool"` | `role: "user"` with `tool_result` block |
| **Usage** | `prompt_tokens`, `completion_tokens` | `input_tokens`, `output_tokens` |
| **Finish Reason** | `finish_reason` | `stop_reason` |
| **Auth Header** | `Authorization: Bearer` | `x-api-key` |

#### Streaming Differences

**OpenAI Streaming:**
```
data: {"choices":[{"delta":{"content":"Hello"}}]}
```

**Anthropic Streaming:**
```
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}
```

### Recommendations

1. **Keep Adapter Pattern**: Maintain the adapter abstraction for OpenAI/Anthropic compatibility

2. **Use OpenAI SDK for OpenAI Provider**: Create a new `OpenAI_SDK_Adapter` that wraps the official SDK

3. **Preserve Manual HTTP for Anthropic**: Keep current `AnthropicAdapter` implementation

4. **Unified Response Format**: Continue using `AdapterResponse` to abstract differences

5. **Backward Compatibility**: Support both manual HTTP and SDK modes via configuration

6. **Tool Definition Conversion**: Continue supporting both `parameters` and `input_schema` keys for compatibility

---

## Summary

The current message schema uses a flexible adapter-based architecture that:

- Separates API communication logic (`APICommunicationService`)
- Abstracts provider-specific formatting (adapters)
- Provides unified response format (`AdapterResponse`)
- Supports both OpenAI and Anthropic APIs
- Handles tool calling in both formats
- Supports streaming and non-streaming modes

For OpenAI SDK integration, the recommended approach is to create a new adapter that wraps the SDK while preserving the existing adapter interface and unified response format. This allows gradual migration and maintains compatibility with Anthropic and other OpenAI-compatible providers.
