Skip to content

Commit d6ade4d

Browse files
authored
Merge pull request #15 from Azure-Samples/thinkchange
Changes for new thinking format and URL format
2 parents f0d24ad + 6e52bab commit d6ade4d

File tree

8 files changed

+65
-63
lines changed

8 files changed

+65
-63
lines changed

.github/workflows/python-check.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
os: ["ubuntu-20.04", "windows-latest"]
16+
os: ["ubuntu-latest", "windows-latest"]
1717
python_version: ["3.11"]
1818
steps:
1919
- uses: actions/checkout@v4

infra/main.bicep

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ module aca 'aca.bicep' = {
123123
containerAppsEnvironmentName: containerApps.outputs.environmentName
124124
containerRegistryName: containerApps.outputs.registryName
125125
aiServicesDeploymentName: aiServicesDeploymentName
126-
aiServicesEndpoint: 'https://${aiServices.outputs.name}.services.ai.azure.com/models'
126+
aiServicesEndpoint: 'https://${aiServices.outputs.name}.services.ai.azure.com'
127127
exists: acaExists
128128
}
129129
}
@@ -167,7 +167,7 @@ output AZURE_LOCATION string = location
167167
output AZURE_TENANT_ID string = tenant().tenantId
168168

169169
output AZURE_DEEPSEEK_DEPLOYMENT string = aiServicesDeploymentName
170-
output AZURE_INFERENCE_ENDPOINT string = 'https://${aiServices.outputs.name}.services.ai.azure.com/models'
170+
output AZURE_INFERENCE_ENDPOINT string = 'https://${aiServices.outputs.name}.services.ai.azure.com'
171171

172172
output SERVICE_ACA_IDENTITY_PRINCIPAL_ID string = aca.outputs.identityPrincipalId
173173
output SERVICE_ACA_NAME string = aca.outputs.name

src/quartapp/chat.py

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import json
22
import os
33

4-
import httpx
54
from azure.identity.aio import AzureDeveloperCliCredential, ManagedIdentityCredential, get_bearer_token_provider
6-
from openai import AsyncOpenAI, DefaultAsyncHttpxClient
5+
from openai import AsyncAzureOpenAI
76
from quart import (
87
Blueprint,
98
Response,
@@ -32,21 +31,11 @@ async def configure_openai():
3231
bp.azure_credential, "https://cognitiveservices.azure.com/.default"
3332
)
3433

35-
class TokenBasedAuth(httpx.Auth):
36-
async def async_auth_flow(self, request):
37-
token = await openai_token_provider()
38-
request.headers["Authorization"] = f"Bearer {token}"
39-
yield request
40-
41-
def sync_auth_flow(self, request):
42-
raise RuntimeError("Cannot use a sync authentication class with httpx.AsyncClient")
43-
4434
# Create the Asynchronous Azure OpenAI client
45-
bp.openai_client = AsyncOpenAI(
46-
base_url=os.environ["AZURE_INFERENCE_ENDPOINT"],
47-
api_key="placeholder",
48-
default_query={"api-version": "2024-05-01-preview"},
49-
http_client=DefaultAsyncHttpxClient(auth=TokenBasedAuth()),
35+
bp.openai_client = AsyncAzureOpenAI(
36+
azure_endpoint=os.environ["AZURE_INFERENCE_ENDPOINT"],
37+
azure_ad_token_provider=openai_token_provider,
38+
api_version="2025-04-01-preview", # temporary
5039
)
5140

5241
# Set the model name to the Azure OpenAI model deployment name
@@ -82,29 +71,9 @@ async def response_stream():
8271
)
8372

8473
try:
85-
is_thinking = False
8674
async for update in await chat_coroutine:
8775
if update.choices:
88-
content = update.choices[0].delta.content
89-
if content == "<think>":
90-
is_thinking = True
91-
update.choices[0].delta.content = None
92-
update.choices[0].delta.reasoning_content = ""
93-
elif content == "</think>":
94-
is_thinking = False
95-
update.choices[0].delta.content = None
96-
update.choices[0].delta.reasoning_content = ""
97-
elif content:
98-
if is_thinking:
99-
yield json.dumps(
100-
{"delta": {"content": None, "reasoning_content": content, "role": "assistant"}},
101-
ensure_ascii=False,
102-
) + "\n"
103-
else:
104-
yield json.dumps(
105-
{"delta": {"content": content, "reasoning_content": None, "role": "assistant"}},
106-
ensure_ascii=False,
107-
) + "\n"
76+
yield update.choices[0].model_dump_json() + "\n"
10877
except Exception as e:
10978
current_app.logger.error(e)
11079
yield json.dumps({"error": str(e)}, ensure_ascii=False) + "\n"

src/quartapp/templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
messageDiv.querySelector(".thoughts").style.display = "block";
121121
messageDiv.querySelector(".thoughts-content").innerHTML = converter.makeHtml(thoughts);
122122
}
123-
} else {
123+
} else if (event.delta.content) {
124124
messageDiv.querySelector(".loading-bar").style.display = "none";
125125
answer += event.delta.content;
126126
messageDiv.querySelector(".answer-content").innerHTML = converter.makeHtml(answer);

src/requirements.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ cffi==1.17.1
3838
# via cryptography
3939
charset-normalizer==3.4.0
4040
# via requests
41-
click==8.1.7
41+
click==8.2.1
4242
# via
4343
# flask
4444
# quart
@@ -58,7 +58,7 @@ frozenlist==1.4.1
5858
# aiosignal
5959
gunicorn==23.0.0
6060
# via quartapp (pyproject.toml)
61-
h11==0.14.0
61+
h11==0.16.0
6262
# via
6363
# httpcore
6464
# hypercorn
@@ -68,7 +68,7 @@ h2==4.1.0
6868
# via hypercorn
6969
hpack==4.0.0
7070
# via h2
71-
httpcore==1.0.7
71+
httpcore==1.0.9
7272
# via httpx
7373
httptools==0.6.4
7474
# via quartapp (pyproject.toml)
@@ -156,7 +156,7 @@ typing-extensions==4.12.2
156156
# pydantic-core
157157
urllib3==2.2.3
158158
# via requests
159-
uvicorn==0.32.0
159+
uvicorn==0.34.2
160160
# via quartapp (pyproject.toml)
161161
uvloop==0.20.0 ; sys_platform != "win32" and (sys_platform != "cygwin" and platform_python_implementation != "PyPy")
162162
# via quartapp (pyproject.toml)

tests/conftest.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@pytest.fixture
1111
def mock_openai_chatcompletion(monkeypatch):
1212
class AsyncChatCompletionIterator:
13-
def __init__(self, answer: str):
13+
def __init__(self, reasoning: str, answer: str):
1414
self.chunk_index = 0
1515
self.chunks = [
1616
openai.types.chat.ChatCompletionChunk(
@@ -32,10 +32,41 @@ def __init__(self, answer: str):
3232
],
3333
)
3434
]
35+
reasoning_deltas = reasoning.split(" ")
36+
for reasoning_index, reasoning_delta in enumerate(reasoning_deltas):
37+
# Text completion chunks include whitespace, so we need to add it back in
38+
if reasoning_index > 0:
39+
answer_delta = " " + reasoning_delta
40+
self.chunks.append(
41+
openai.types.chat.ChatCompletionChunk(
42+
id="test-123",
43+
object="chat.completion.chunk",
44+
choices=[
45+
openai.types.chat.chat_completion_chunk.Choice(
46+
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
47+
role=None, reasoning_content=reasoning_delta
48+
),
49+
finish_reason=None,
50+
index=0,
51+
logprobs=None,
52+
# Only Azure includes content_filter_results
53+
content_filter_results={
54+
"hate": {"filtered": False, "severity": "safe"},
55+
"self_harm": {"filtered": False, "severity": "safe"},
56+
"sexual": {"filtered": False, "severity": "safe"},
57+
"violence": {"filtered": False, "severity": "safe"},
58+
},
59+
)
60+
],
61+
created=1703462735,
62+
model="DeepSeek-R1",
63+
)
64+
)
65+
3566
answer_deltas = answer.split(" ")
3667
for answer_index, answer_delta in enumerate(answer_deltas):
3768
# Text completion chunks include whitespace, so we need to add it back in
38-
if answer_index > 0 and answer_delta != "</think>":
69+
if answer_index > 0:
3970
answer_delta = " " + answer_delta
4071
self.chunks.append(
4172
openai.types.chat.ChatCompletionChunk(
@@ -95,9 +126,9 @@ async def mock_acreate(*args, **kwargs):
95126
# Only mock a stream=True completion
96127
last_message = kwargs.get("messages")[-1]["content"]
97128
if last_message == "What is the capital of France?":
98-
return AsyncChatCompletionIterator("<think> hmm </think> The capital of France is Paris.")
129+
return AsyncChatCompletionIterator("hmm", "The capital of France is Paris.")
99130
elif last_message == "What is the capital of Germany?":
100-
return AsyncChatCompletionIterator("<think> hmm </think> The capital of Germany is Berlin.")
131+
return AsyncChatCompletionIterator("hmm", "The capital of Germany is Berlin.")
101132
else:
102133
raise ValueError(f"Unexpected message: {last_message}")
103134

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
{"delta": {"content": null, "reasoning_content": " hmm", "role": "assistant"}}
2-
{"delta": {"content": " The", "reasoning_content": null, "role": "assistant"}}
3-
{"delta": {"content": " capital", "reasoning_content": null, "role": "assistant"}}
4-
{"delta": {"content": " of", "reasoning_content": null, "role": "assistant"}}
5-
{"delta": {"content": " France", "reasoning_content": null, "role": "assistant"}}
6-
{"delta": {"content": " is", "reasoning_content": null, "role": "assistant"}}
7-
{"delta": {"content": " Paris.", "reasoning_content": null, "role": "assistant"}}
1+
{"delta":{"content":null,"function_call":null,"refusal":null,"role":null,"tool_calls":null,"reasoning_content":"hmm"},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
2+
{"delta":{"content":"The","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
3+
{"delta":{"content":" capital","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
4+
{"delta":{"content":" of","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
5+
{"delta":{"content":" France","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
6+
{"delta":{"content":" is","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
7+
{"delta":{"content":" Paris.","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
8+
{"delta":{"content":null,"function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":"stop","index":0,"logprobs":null,"content_filter_results":{}}
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
{"delta": {"content": null, "reasoning_content": " hmm", "role": "assistant"}}
2-
{"delta": {"content": " The", "reasoning_content": null, "role": "assistant"}}
3-
{"delta": {"content": " capital", "reasoning_content": null, "role": "assistant"}}
4-
{"delta": {"content": " of", "reasoning_content": null, "role": "assistant"}}
5-
{"delta": {"content": " Germany", "reasoning_content": null, "role": "assistant"}}
6-
{"delta": {"content": " is", "reasoning_content": null, "role": "assistant"}}
7-
{"delta": {"content": " Berlin.", "reasoning_content": null, "role": "assistant"}}
1+
{"delta":{"content":null,"function_call":null,"refusal":null,"role":null,"tool_calls":null,"reasoning_content":"hmm"},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
2+
{"delta":{"content":"The","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
3+
{"delta":{"content":" capital","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
4+
{"delta":{"content":" of","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
5+
{"delta":{"content":" Germany","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
6+
{"delta":{"content":" is","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
7+
{"delta":{"content":" Berlin.","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}
8+
{"delta":{"content":null,"function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":"stop","index":0,"logprobs":null,"content_filter_results":{}}

0 commit comments

Comments
 (0)