diff --git a/CLAUDE.md b/CLAUDE.md
index 2b5ef3c2f2bcc7df501fff53d2ed30b738281412..ae1ed27ee1601dad1d87437d9487bee77b02ec9b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -38,7 +38,7 @@ cmd/cc-proxy  →  server  →  oai (bridge)  →  cchat (subprocess)  →  ccwi
 - **ccwire** — Wire format types (`SystemMessage`, `AssistantMessage`, `ResultMessage`, `StreamEventMessage`) and NDJSON parser for Claude Code CLI stdout
 - **cchat** — Wraps the `claude` CLI as a subprocess. `Client` manages concurrency (buffered channel semaphore). `Stream` reads parsed messages from the process. Prompt is piped via stdin to avoid OS arg length limits
 - **oai** — OpenAI-compatible types (request/response/chunk) plus the bridge layer:
-  - `bridge_request.go`: OAI messages → CC prompt string (roles as `[user]:` prefixed text, tool results as `[tool_result for call_id]:`)
+  - `bridge_request.go`: OAI messages → CC prompt string (`system` and `developer` instructions combined into the system prompt in their original order, conversation roles as `[user]:`/`[assistant]:` prefixed text, tool results as `[tool_result for call_id]:`)
   - `bridge_response.go`: CC result → OAI response (extracts text, parses tool calls, maps usage)
   - `bridge_stream.go`: Streaming state machine with safety margin — holds back last `len("<tool_call>")` bytes to prevent partial XML tag leaks
   - `tools.go`: `ParseToolCalls()` extracts `<tool_call>` XML tags via regex, `ToolCallInstructions()` generates system prompt text for tool use
diff --git a/README.md b/README.md
index 2f4c526933e84783124c3abe8211d888416cdc0d..36a0074367e4d7d4d301138704403437bbe383f9 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,8 @@ POST /v1/chat/completions
 
 Each request is stateless. Spawn process, pipe prompt, stream response, exit. No session state, no connection pools, no daemon. Claude's built-in tools are disabled — your framework provides the tools, cc-proxy just does inference.
 
+Supported message roles: `system`, `developer`, `user`, `assistant`, and `tool`. System and developer instructions are combined into Claude's system prompt in their original order, including title-generation instructions, coding-agent rules, and project context sent by IDEs such as Android Studio.
+
 ---
 
 ## Quick start
diff --git a/oai/bridge_request.go b/oai/bridge_request.go
index cd4b667622222ef622ac6b4be71a164c09df4088..3ce11a18fd56c8fdf6e0deb1856387445becaa9e 100644
--- a/oai/bridge_request.go
+++ b/oai/bridge_request.go
@@ -12,7 +12,8 @@ import (
 // and [cchat.QueryOptions] suitable for [cchat.Client.Query].
 //
 // Messages are translated according to their role:
-//   - "system" messages are concatenated into the system prompt.
+//   - "system" and "developer" messages are concatenated into the system prompt
+//     in their original order.
 //   - "user" messages are prefixed with "[user]: ".
 //   - "assistant" messages are prefixed with "[assistant]: ". If the message
 //     includes ToolCalls, they are re-encoded as <tool_call> XML tags.
@@ -26,7 +27,7 @@ func RequestToQuery(req *ChatCompletionRequest) (prompt string, opts cchat.Query
 
 	for _, msg := range req.Messages {
 		switch msg.Role {
-		case "system":
+		case "system", "developer":
 			systemParts = append(systemParts, msg.StringContent())
 
 		case "user":
diff --git a/oai/bridge_response_test.go b/oai/bridge_response_test.go
index 1fd949de0e1049a0908a334c3aee935f17b1b696..ccdce4d0dfbb467867ad622b972d9b3401d201ba 100644
--- a/oai/bridge_response_test.go
+++ b/oai/bridge_response_test.go
@@ -1,16 +1,45 @@
 package oai
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/codewandler/cc-sdk-go/ccwire"
 )
 
+func TestResultToResponse_ToolCallsOmitIndex(t *testing.T) {
+	result := &ccwire.ResultMessage{
+		Result: `<tool_call>{"name":"ping","arguments":{"value":"hello"}}</tool_call><tool_call>{"name":"pong","arguments":{}}</tool_call>`,
+	}
+	response := ResultToResponse(result, nil, true)
+	toolCalls := response.Choices[0].Message.ToolCalls
+	if len(toolCalls) != 2 {
+		t.Fatalf("len(toolCalls) = %d, want 2", len(toolCalls))
+	}
+
+	for i, call := range toolCalls {
+		if call.Index != nil {
+			t.Errorf("toolCalls[%d].Index = %d, want nil", i, *call.Index)
+		}
+		data, err := json.Marshal(call)
+		if err != nil {
+			t.Fatalf("marshal tool call: %v", err)
+		}
+		var fields map[string]json.RawMessage
+		if err := json.Unmarshal(data, &fields); err != nil {
+			t.Fatalf("unmarshal tool call: %v", err)
+		}
+		if _, ok := fields["index"]; ok {
+			t.Errorf("toolCalls[%d] should omit index from JSON: %s", i, data)
+		}
+	}
+}
+
 func TestExtractText(t *testing.T) {
 	tests := []struct {
-		name     string
+		name      string
 		assistant *ccwire.AssistantMessage
-		want     string
+		want      string
 	}{
 		{
 			name: "single_text_block",
diff --git a/oai/bridge_stream.go b/oai/bridge_stream.go
index 7a9c497102753c69c8c18ed06eb097fb9467efdd..8a5ed4735fa8d29ab66d386cee3abd95c098c24d 100644
--- a/oai/bridge_stream.go
+++ b/oai/bridge_stream.go
@@ -32,9 +32,9 @@ type StreamState struct {
 	Model     string
 	Created   int64
 	HasTools  bool
-	Buffering bool           // true when we've detected <tool_call in the buffer
+	Buffering bool            // true when we've detected <tool_call in the buffer
 	buffer    strings.Builder // accumulated text (always appended when HasTools)
-	Emitted   int            // number of bytes of buffer already streamed to client
+	Emitted   int             // number of bytes of buffer already streamed to client
 }
 
 // NewStreamState creates a new StreamState for a streaming response.
@@ -123,7 +123,10 @@ func (ss *StreamState) FinishChunk(assistant *ccwire.AssistantMessage) []*ChatCo
 				chunks = append(chunks, ss.makeContentChunk(&remainder))
 			}
 
-			// Emit tool calls
+			// Emit tool calls with sequential streaming indices
+			for i := range toolCalls {
+				toolCalls[i].Index = &i
+			}
 			reason := "tool_calls"
 			chunks = append(chunks, &ChatCompletionChunk{
 				ID:      ss.ID,
diff --git a/oai/bridge_stream_test.go b/oai/bridge_stream_test.go
index 0214853fd8a2abe9e436ecf73258d31fa0549716..88490c0514d05e9967f1866c1176988c740eb700 100644
--- a/oai/bridge_stream_test.go
+++ b/oai/bridge_stream_test.go
@@ -1,6 +1,7 @@
 package oai
 
 import (
+	"encoding/json"
 	"strings"
 	"testing"
 
@@ -357,6 +358,57 @@ func TestStreamState_FinishChunk_WithTools_MultipleToolCalls(t *testing.T) {
 	}
 }
 
+func TestStreamState_FinishChunk_ToolCallIndices(t *testing.T) {
+	tests := []struct {
+		name      string
+		text      string
+		wantCalls int
+	}{
+		{
+			name:      "single_tool_call",
+			text:      `<tool_call>{"name":"ping","arguments":{"value":"hello"}}</tool_call>`,
+			wantCalls: 1,
+		},
+		{
+			name:      "multiple_tool_calls",
+			text:      `<tool_call>{"name":"tool_a","arguments":{}}</tool_call><tool_call>{"name":"tool_b","arguments":{}}</tool_call><tool_call>{"name":"tool_c","arguments":{}}</tool_call>`,
+			wantCalls: 3,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ss := NewStreamState(true)
+			ss.TextDeltaChunk(tt.text)
+			chunks := ss.FinishChunk(nil)
+			if len(chunks) != 1 {
+				t.Fatalf("len(chunks) = %d, want 1", len(chunks))
+			}
+
+			data, err := json.Marshal(chunks[0])
+			if err != nil {
+				t.Fatalf("marshal chunk: %v", err)
+			}
+			var chunk ChatCompletionChunk
+			if err := json.Unmarshal(data, &chunk); err != nil {
+				t.Fatalf("unmarshal chunk: %v", err)
+			}
+
+			toolCalls := chunk.Choices[0].Delta.ToolCalls
+			if len(toolCalls) != tt.wantCalls {
+				t.Fatalf("len(toolCalls) = %d, want %d", len(toolCalls), tt.wantCalls)
+			}
+			for i, call := range toolCalls {
+				if call.Index == nil {
+					t.Errorf("toolCalls[%d].Index is missing from JSON: %s", i, data)
+				} else if *call.Index != i {
+					t.Errorf("toolCalls[%d].Index = %d, want %d", i, *call.Index, i)
+				}
+			}
+		})
+	}
+}
+
 func TestStreamState_HandleStreamEvent_MessageStart(t *testing.T) {
 	ss := NewStreamState(false)
 
diff --git a/oai/client_test.go b/oai/client_test.go
index d982f171aad06d38fe6e21ecd7b411f706ca3531..88631503a9256458cd230ad97a3abc7cf2f7761d 100644
--- a/oai/client_test.go
+++ b/oai/client_test.go
@@ -61,6 +61,32 @@ var cases = []testCase{
 			requireFinish(t, resp, "stop")
 		},
 	},
+	{
+		Name: "developer_title",
+		Request: oai.ChatCompletionRequest{
+			Model: "haiku",
+			Messages: []oai.ChatMessage{
+				{Role: "developer", Content: "Summarize the user's query into a 5–10 word conversation title. Return only the title; do not answer the query."},
+				{Role: "user", Content: "List all gradle modules in this project"},
+			},
+		},
+		Validate: func(t *testing.T, resp *oai.ChatCompletionResponse) {
+			requireContent(t, resp)
+			title := strings.TrimSpace(resp.Choices[0].Message.StringContent())
+			t.Logf("title: %s", title)
+			if words := len(strings.Fields(title)); words < 5 || words > 10 {
+				t.Errorf("expected a 5–10 word title, got %d words: %q", words, title)
+			}
+			lower := strings.ToLower(title)
+			if !strings.Contains(lower, "gradle") || !strings.Contains(lower, "modules") {
+				t.Errorf("expected a title about Gradle modules, got: %q", title)
+			}
+			if strings.Contains(title, "\n") || strings.HasPrefix(lower, "i ") || strings.HasPrefix(lower, "i'") {
+				t.Errorf("expected only a title, got: %q", title)
+			}
+			requireFinish(t, resp, "stop")
+		},
+	},
 	{
 		Name: "multi_turn",
 		Request: oai.ChatCompletionRequest{
diff --git a/oai/request.go b/oai/request.go
index 28f14e46e82234c7ac192dbdd852c97324246504..01c5162f7af959bae961dd5d65f8448bd4741694 100644
--- a/oai/request.go
+++ b/oai/request.go
@@ -58,22 +58,22 @@ import "encoding/json"
 // Fields like Temperature, TopP, Stop, and N are accepted for API compatibility
 // but are not forwarded to the Claude Code CLI.
 type ChatCompletionRequest struct {
-	Model            string        `json:"model"`
-	Messages         []ChatMessage `json:"messages"`
-	Stream           bool          `json:"stream,omitempty"`
-	Temperature      *float64      `json:"temperature,omitempty"`
-	MaxTokens        *int          `json:"max_tokens,omitempty"`
-	MaxCompletionTokens *int       `json:"max_completion_tokens,omitempty"`
-	Tools            []Tool        `json:"tools,omitempty"`
-	ToolChoice       any           `json:"tool_choice,omitempty"`
-	Stop             any           `json:"stop,omitempty"`
-	TopP             *float64      `json:"top_p,omitempty"`
-	N                *int          `json:"n,omitempty"`
-	User             string        `json:"user,omitempty"`
+	Model               string        `json:"model"`
+	Messages            []ChatMessage `json:"messages"`
+	Stream              bool          `json:"stream,omitempty"`
+	Temperature         *float64      `json:"temperature,omitempty"`
+	MaxTokens           *int          `json:"max_tokens,omitempty"`
+	MaxCompletionTokens *int          `json:"max_completion_tokens,omitempty"`
+	Tools               []Tool        `json:"tools,omitempty"`
+	ToolChoice          any           `json:"tool_choice,omitempty"`
+	Stop                any           `json:"stop,omitempty"`
+	TopP                *float64      `json:"top_p,omitempty"`
+	N                   *int          `json:"n,omitempty"`
+	User                string        `json:"user,omitempty"`
 }
 
 // ChatMessage represents a single message in the conversation history.
-// Role must be one of "system", "user", "assistant", or "tool".
+// Role must be one of "system", "developer", "user", "assistant", or "tool".
 //
 // Content may be either a plain string or an array of [ContentPart] objects
 // (for multi-part messages). Use [ChatMessage.StringContent] to extract the
@@ -83,7 +83,7 @@ type ChatCompletionRequest struct {
 // the structured calls. For tool-role messages returning results, ToolCallID
 // identifies which call this result corresponds to.
 type ChatMessage struct {
-	Role       string     `json:"role"` // "system", "user", "assistant", "tool"
+	Role       string     `json:"role"` // "system", "developer", "user", "assistant", "tool"
 	Content    any        `json:"content,omitempty"`
 	Name       string     `json:"name,omitempty"`
 	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
@@ -156,7 +156,9 @@ type FunctionDefinition struct {
 // ID is a unique identifier (prefixed with "call_") generated during parsing.
 // Type is always "function". These are produced by [ParseToolCalls] from
 // <tool_call> XML tags in the model output.
+// Index is set only for streaming tool calls.
 type ToolCall struct {
+	Index    *int         `json:"index,omitempty"`
 	ID       string       `json:"id"`
 	Type     string       `json:"type"` // "function"
 	Function FunctionCall `json:"function"`
diff --git a/oai/bridge_request_test.go b/oai/bridge_request_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1732df68dec067a2bae952c9afba6c167b972301
--- /dev/null
+++ b/oai/bridge_request_test.go
@@ -0,0 +1,63 @@
+package oai
+
+import "testing"
+
+func TestRequestToQuery_InstructionRoles(t *testing.T) {
+	tests := []struct {
+		name       string
+		messages   []ChatMessage
+		wantSystem string
+	}{
+		{
+			name: "developer_only",
+			messages: []ChatMessage{
+				{Role: "developer", Content: "Summarize the user's query into a 5–10 word title."},
+				{Role: "developer", Content: "Return only the title; do not answer the query."},
+				{Role: "user", Content: "List all gradle modules in this project"},
+			},
+			wantSystem: "Summarize the user's query into a 5–10 word title.\n\nReturn only the title; do not answer the query.",
+		},
+		{
+			name: "system_only",
+			messages: []ChatMessage{
+				{Role: "system", Content: "You are a coding assistant."},
+				{Role: "system", Content: "Keep responses concise."},
+				{Role: "user", Content: "List all gradle modules in this project"},
+			},
+			wantSystem: "You are a coding assistant.\n\nKeep responses concise.",
+		},
+		{
+			name: "mixed_system_first",
+			messages: []ChatMessage{
+				{Role: "system", Content: "You are a coding assistant."},
+				{Role: "developer", Content: "Follow the IDE tool usage rules."},
+				{Role: "user", Content: "List all gradle modules in this project"},
+				{Role: "system", Content: "Keep responses concise."},
+				{Role: "developer", Content: "Project context: this is an Android project."},
+			},
+			wantSystem: "You are a coding assistant.\n\nFollow the IDE tool usage rules.\n\nKeep responses concise.\n\nProject context: this is an Android project.",
+		},
+		{
+			name: "mixed_developer_first",
+			messages: []ChatMessage{
+				{Role: "developer", Content: "Follow the IDE tool usage rules."},
+				{Role: "system", Content: "You are a coding assistant."},
+				{Role: "developer", Content: "Project context: this is an Android project."},
+				{Role: "user", Content: "List all gradle modules in this project"},
+			},
+			wantSystem: "Follow the IDE tool usage rules.\n\nYou are a coding assistant.\n\nProject context: this is an Android project.",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			prompt, opts := RequestToQuery(&ChatCompletionRequest{Messages: tt.messages})
+			if opts.SystemPrompt != tt.wantSystem {
+				t.Errorf("SystemPrompt = %q, want %q", opts.SystemPrompt, tt.wantSystem)
+			}
+			if want := "[user]: List all gradle modules in this project"; prompt != want {
+				t.Errorf("prompt = %q, want %q", prompt, want)
+			}
+		})
+	}
+}
