Commit f182fbd1 authored by Your Name's avatar Your Name

All working but qwen-oauth2

parent 8b50a0ae
{
"name": ".kilo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
...@@ -25,7 +25,7 @@ import json ...@@ -25,7 +25,7 @@ import json
import logging import logging
import time import time
import uuid import uuid
from typing import Dict, List, Optional, Union, AsyncIterator from typing import Dict, List, Optional, Union, AsyncIterator, Tuple
from openai import OpenAI from openai import OpenAI
import httpx import httpx
...@@ -49,8 +49,8 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -49,8 +49,8 @@ class CodexProviderHandler(BaseProviderHandler):
- Standard OpenAI protocol with Bearer token - Standard OpenAI protocol with Bearer token
**OAuth2 Mode** (no api_key, OAuth2 credentials): **OAuth2 Mode** (no api_key, OAuth2 credentials):
- Uses ChatGPT backend API: https://chatgpt.com/backend-api/codex - Uses ChatGPT backend API: https://chatgpt.com/backend-api
- Uses Responses API endpoint: /v1/responses - Uses Responses API endpoint: /codex/responses
- ChatGPT-specific protocol with SSE streaming - ChatGPT-specific protocol with SSE streaming
- Includes ChatGPT-Account-ID header - Includes ChatGPT-Account-ID header
...@@ -104,7 +104,7 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -104,7 +104,7 @@ class CodexProviderHandler(BaseProviderHandler):
# OAuth2 Mode: Check if OAuth2 is authenticated # OAuth2 Mode: Check if OAuth2 is authenticated
# If authenticated, use ChatGPT backend; otherwise use configured endpoint # If authenticated, use ChatGPT backend; otherwise use configured endpoint
if self.oauth2.is_authenticated(): if self.oauth2.is_authenticated():
self.base_url = "https://chatgpt.com/backend-api/codex" self.base_url = "https://chatgpt.com/backend-api"
logger.info(f"CodexProviderHandler: Initialized in OAuth2 mode with ChatGPT backend: {self.base_url}") logger.info(f"CodexProviderHandler: Initialized in OAuth2 mode with ChatGPT backend: {self.base_url}")
else: else:
# Not yet authenticated, keep configured endpoint # Not yet authenticated, keep configured endpoint
...@@ -160,8 +160,8 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -160,8 +160,8 @@ class CodexProviderHandler(BaseProviderHandler):
self._account_id = self.oauth2.credentials['tokens'].get('account_id') self._account_id = self.oauth2.credentials['tokens'].get('account_id')
# Switch to ChatGPT backend if OAuth2 is now authenticated # Switch to ChatGPT backend if OAuth2 is now authenticated
if not self._use_api_key_mode and self.base_url != "https://chatgpt.com/backend-api/codex": if not self._use_api_key_mode and self.base_url != "https://chatgpt.com/backend-api":
self.base_url = "https://chatgpt.com/backend-api/codex" self.base_url = "https://chatgpt.com/backend-api"
logger.info(f"CodexProviderHandler: Switched to ChatGPT backend after OAuth2 authentication: {self.base_url}") logger.info(f"CodexProviderHandler: Switched to ChatGPT backend after OAuth2 authentication: {self.base_url}")
# Update the configuration with the new endpoint # Update the configuration with the new endpoint
...@@ -257,19 +257,35 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -257,19 +257,35 @@ class CodexProviderHandler(BaseProviderHandler):
# OAuth2 Mode Methods (ChatGPT Responses API) # OAuth2 Mode Methods (ChatGPT Responses API)
# ========================================================================= # =========================================================================
def _convert_messages_to_responses_format(self, messages: List[Dict]) -> List[Dict]: def _convert_messages_to_responses_format(self, messages: List[Dict]) -> Tuple[List[Dict], Optional[str]]:
""" """
Convert OpenAI Chat Completions messages to Responses API format. Convert OpenAI Chat Completions messages to Responses API format.
OpenAI format: {"role": "user", "content": "text"} OpenAI format: {"role": "user", "content": "text"}
Responses format: {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "text"}]} Responses format: {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "text"}]}
Returns:
tuple: (converted_messages, system_instruction)
- converted_messages: List of messages in Responses API format
- system_instruction: Combined system message content (if any)
""" """
result = [] result = []
system_instructions = []
for msg in messages: for msg in messages:
role = msg.get("role", "user") role = msg.get("role", "user")
content = msg.get("content", "") content = msg.get("content", "")
# Handle system messages - extract for instructions field
if role == "system":
if isinstance(content, str):
system_instructions.append(content)
elif isinstance(content, list):
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
system_instructions.append(item.get("text", ""))
continue
# Handle tool messages # Handle tool messages
if role == "tool": if role == "tool":
result.append({ result.append({
...@@ -299,17 +315,17 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -299,17 +315,17 @@ class CodexProviderHandler(BaseProviderHandler):
}) })
continue continue
# Handle regular messages # Handle regular messages (user, developer, assistant)
content_items = [] content_items = []
if isinstance(content, str): if isinstance(content, str):
content_type = "input_text" if role in ["user", "system", "developer"] else "output_text" content_type = "input_text" if role in ["user", "developer"] else "output_text"
content_items.append({"type": content_type, "text": content}) content_items.append({"type": content_type, "text": content})
elif isinstance(content, list): elif isinstance(content, list):
# Handle multimodal content # Handle multimodal content
for item in content: for item in content:
if isinstance(item, dict): if isinstance(item, dict):
if item.get("type") == "text": if item.get("type") == "text":
content_type = "input_text" if role in ["user", "system", "developer"] else "output_text" content_type = "input_text" if role in ["user", "developer"] else "output_text"
content_items.append({"type": content_type, "text": item.get("text", "")}) content_items.append({"type": content_type, "text": item.get("text", "")})
elif item.get("type") == "image_url": elif item.get("type") == "image_url":
content_items.append({ content_items.append({
...@@ -324,7 +340,40 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -324,7 +340,40 @@ class CodexProviderHandler(BaseProviderHandler):
"content": content_items "content": content_items
}) })
return result # Combine system instructions
combined_system = " ".join(system_instructions) if system_instructions else None
return result, combined_system
def _convert_tools_to_codex_format(self, tools: Optional[List[Dict]]) -> List[Dict]:
"""
Convert OpenAI tool format to Codex/ChatGPT format.
OpenAI format: {"type": "function", "function": {"name": "...", "description": "...", "parameters": {...}}}
Codex format: {"type": "function", "name": "...", "description": "...", "parameters": {...}}
Key difference: No nested "function" object in Codex format.
"""
if not tools:
return []
converted_tools = []
for tool in tools:
if tool.get("type") == "function" and "function" in tool:
# OpenAI format - flatten it
func = tool["function"]
converted_tool = {
"type": "function",
"name": func.get("name"),
"description": func.get("description", ""),
"parameters": func.get("parameters", {}),
}
converted_tools.append(converted_tool)
else:
# Already in Codex format or other type
converted_tools.append(tool)
return converted_tools
def _build_responses_request( def _build_responses_request(
self, self,
...@@ -336,29 +385,32 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -336,29 +385,32 @@ class CodexProviderHandler(BaseProviderHandler):
tool_choice: Optional[Union[str, Dict]] = None, tool_choice: Optional[Union[str, Dict]] = None,
) -> Dict: ) -> Dict:
"""Build a Responses API request payload.""" """Build a Responses API request payload."""
# Convert messages to Responses format # Convert messages to Responses format and extract system instructions
input_items = self._convert_messages_to_responses_format(messages) input_items, system_instruction = self._convert_messages_to_responses_format(messages)
# Use system instruction from messages if available, otherwise use default
instructions = system_instruction if system_instruction else "You are Codex, a helpful AI assistant for coding tasks."
# Convert tools to Codex format (flatten the structure)
codex_tools = self._convert_tools_to_codex_format(tools)
# Build base request # Build base request
request = { request = {
"model": model, "model": model,
"instructions": "You are Codex, a helpful AI assistant for coding tasks.", "instructions": instructions,
"input": input_items, "input": input_items,
"stream": True, "stream": True,
"store": False, "store": False,
"tools": codex_tools,
"tool_choice": "auto",
"parallel_tool_calls": True,
} }
# Add optional parameters # Add optional parameters
if max_tokens is not None: # Note: temperature and max_tokens are not supported by /codex/responses endpoint
request["max_tokens"] = max_tokens # They are handled internally by the model
if temperature is not None:
request["temperature"] = temperature
if tools:
# Convert OpenAI tool format to Responses API format
request["tools"] = tools
# Override tool_choice if explicitly provided
if tool_choice: if tool_choice:
request["tool_choice"] = tool_choice if isinstance(tool_choice, str) else "auto" request["tool_choice"] = tool_choice if isinstance(tool_choice, str) else "auto"
...@@ -506,16 +558,24 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -506,16 +558,24 @@ class CodexProviderHandler(BaseProviderHandler):
headers = self._build_headers(api_key, conversation_id) headers = self._build_headers(api_key, conversation_id)
# Make request to Responses API # Make request to Responses API
url = f"{self.base_url}/v1/responses" url = f"{self.base_url}/codex/responses"
logger.info(f"CodexProviderHandler: Sending request to {url}") logger.info(f"CodexProviderHandler: Sending request to {url}")
if AISBF_DEBUG:
logger.info(f"CodexProviderHandler: Request payload: {json.dumps(request_payload, indent=2)}")
logger.info(f"CodexProviderHandler: Request headers: {json.dumps({k: v for k, v in headers.items() if k.lower() != 'authorization'}, indent=2)}")
async with httpx.AsyncClient(timeout=300.0) as client: async with httpx.AsyncClient(timeout=300.0) as client:
response = await client.post( async with client.stream(
"POST",
url, url,
headers=headers, headers=headers,
json=request_payload, json=request_payload,
) ) as response:
if response.status_code >= 400:
error_body = await response.aread()
logger.error(f"CodexProviderHandler: Error response status: {response.status_code}")
logger.error(f"CodexProviderHandler: Error response body: {error_body.decode('utf-8')}")
response.raise_for_status() response.raise_for_status()
......
...@@ -25,6 +25,7 @@ import asyncio ...@@ -25,6 +25,7 @@ import asyncio
import time import time
import json import json
import platform import platform
import uuid
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from openai import AsyncOpenAI from openai import AsyncOpenAI
from ..models import Model from ..models import Model
...@@ -130,19 +131,35 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -130,19 +131,35 @@ class QwenProviderHandler(BaseProviderHandler):
logger.info("QwenProviderHandler: Using OAuth2 authentication") logger.info("QwenProviderHandler: Using OAuth2 authentication")
auth_key = access_token auth_key = access_token
# Use provider configured endpoint for OAuth2 (fixed endpoints) # Get resource URL from auth and normalize it properly
base_url = self.provider_config.endpoint base_url = self.auth.get_resource_url()
# Normalize endpoint # Normalize endpoint exactly as specified in documentation
if not base_url.startswith("http"): if not base_url.startswith("http"):
base_url = f"https://{base_url}" base_url = f"https://{base_url}"
# DashScope endpoint already includes /v1 so do not append again
if not base_url.endswith("/v1"):
base_url = f"{base_url}/v1"
logger.info(f"QwenProviderHandler: Final endpoint: {base_url}")
# Build required DashScope headers
import uuid
user_agent = f"QwenCode/1.0.0 ({platform.system().lower()}; {platform.machine()})"
default_headers = {
"Accept": "application/json",
"X-DashScope-CacheControl": "enable",
"X-DashScope-UserAgent": user_agent,
"X-DashScope-AuthType": "qwen-oauth",
"x-request-id": str(uuid.uuid4()),
}
self._sdk_client = AsyncOpenAI( self._sdk_client = AsyncOpenAI(
api_key=auth_key, api_key=auth_key,
base_url=base_url, base_url=base_url,
max_retries=3, max_retries=3,
timeout=httpx.Timeout(300.0, connect=30.0), timeout=httpx.Timeout(300.0, connect=30.0),
default_headers=default_headers,
) )
logger.info(f"QwenProviderHandler: Created SDK client (endpoint: {base_url})") logger.info(f"QwenProviderHandler: Created SDK client (endpoint: {base_url})")
...@@ -223,12 +240,22 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -223,12 +240,22 @@ class QwenProviderHandler(BaseProviderHandler):
# Get SDK client with current OAuth token # Get SDK client with current OAuth token
client = await self._get_sdk_client() client = await self._get_sdk_client()
# Generate session tracking IDs
session_id = str(uuid.uuid4())
prompt_id = str(uuid.uuid4())
# Build request parameters # Build request parameters
request_params = { request_params = {
"model": model, "model": model,
"messages": messages, "messages": messages,
"max_tokens": max_tokens or 4096, "max_tokens": max_tokens or 4096,
"stream": stream, "stream": stream,
"extra_body": {
"metadata": {
"sessionId": session_id,
"promptId": prompt_id
}
}
} }
if temperature is not None and temperature > 0: if temperature is not None and temperature > 0:
...@@ -240,6 +267,10 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -240,6 +267,10 @@ class QwenProviderHandler(BaseProviderHandler):
if tool_choice and tools: if tool_choice and tools:
request_params["tool_choice"] = tool_choice request_params["tool_choice"] = tool_choice
# Add stream_options for streaming requests
if stream:
request_params["stream_options"] = {"include_usage": True}
try: try:
if stream: if stream:
logger.info("QwenProviderHandler: Using streaming mode") logger.info("QwenProviderHandler: Using streaming mode")
...@@ -440,16 +471,37 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -440,16 +471,37 @@ class QwenProviderHandler(BaseProviderHandler):
using_api_key = qwen_config and isinstance(qwen_config, dict) and qwen_config.get('api_key') using_api_key = qwen_config and isinstance(qwen_config, dict) and qwen_config.get('api_key')
if not using_api_key: if not using_api_key:
# OAuth2 authentication: return fixed model list # OAuth2 authentication: return full model list
logger.info("QwenProviderHandler: Using OAuth2 authentication, returning fixed model list") logger.info("QwenProviderHandler: Using OAuth2 authentication, returning full model list")
return [ return [
Model( Model(
id="coder-model", id="qwen-turbo",
name="Coder Model", name="Qwen Turbo",
provider_id=self.provider_id, provider_id=self.provider_id,
context_size=1000000, context_size=32000,
context_length=1000000, context_length=32000,
) ),
Model(
id="qwen-plus",
name="Qwen Plus",
provider_id=self.provider_id,
context_size=128000,
context_length=128000,
),
Model(
id="qwen-max",
name="Qwen Max",
provider_id=self.provider_id,
context_size=128000,
context_length=128000,
),
Model(
id="qwen3-coder-plus",
name="Qwen 3 Coder Plus",
provider_id=self.provider_id,
context_size=128000,
context_length=128000,
),
] ]
# API token authentication: fetch from models endpoint # API token authentication: fetch from models endpoint
...@@ -502,10 +554,10 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -502,10 +554,10 @@ class QwenProviderHandler(BaseProviderHandler):
# Fallback to static model list # Fallback to static model list
logger.warning("QwenProviderHandler: No models returned from API, using static list") logger.warning("QwenProviderHandler: No models returned from API, using static list")
models = [ models = [
Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=32000), Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=32000),
Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=8000), Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=128000),
Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=8000), Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=128000),
Model(id="coder-model", name="Qwen Coder", provider_id=self.provider_id, context_size=32000), Model(id="qwen3-coder-plus", name="Qwen 3 Coder Plus", provider_id=self.provider_id, context_size=128000),
] ]
logger.info(f"QwenProviderHandler: Returning {len(models)} models") logger.info(f"QwenProviderHandler: Returning {len(models)} models")
...@@ -517,8 +569,8 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -517,8 +569,8 @@ class QwenProviderHandler(BaseProviderHandler):
# Return static fallback list # Return static fallback list
logger.info("QwenProviderHandler: Using static fallback model list") logger.info("QwenProviderHandler: Using static fallback model list")
return [ return [
Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=32000), Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=32000),
Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=8000), Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=128000),
Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=8000), Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=128000),
Model(id="coder-model", name="Qwen Coder", provider_id=self.provider_id, context_size=32000), Model(id="qwen3-coder-plus", name="Qwen 3 Coder Plus", provider_id=self.provider_id, context_size=128000),
] ]
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment