@@ -12,6 +12,25 @@ public partial class Tests
1212 return Environment . GetEnvironmentVariable ( "OPENAI_API_KEY" ) ;
1313 }
1414
15+ private static async Task < int > GetServerTokenCount ( HttpClient httpClient , string requestJson )
16+ {
17+ var response = await httpClient . PostAsync (
18+ "https://api.openai.com/v1/responses/input_tokens" ,
19+ new StringContent ( requestJson , Encoding . UTF8 , "application/json" ) ) ;
20+
21+ response . EnsureSuccessStatusCode ( ) ;
22+ var responseBody = await response . Content . ReadAsStringAsync ( ) ;
23+ using var doc = JsonDocument . Parse ( responseBody ) ;
24+ return doc . RootElement . GetProperty ( "input_tokens" ) . GetInt32 ( ) ;
25+ }
26+
27+ private static HttpClient CreateOpenAiClient ( string apiKey )
28+ {
29+ var httpClient = new HttpClient ( ) ;
30+ httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , apiKey ) ;
31+ return httpClient ;
32+ }
33+
1534 [ TestMethod ]
1635 public async Task ValidateMessageTokensAgainstOpenAiApi ( )
1736 {
@@ -22,31 +41,20 @@ public async Task ValidateMessageTokensAgainstOpenAiApi()
2241 return ;
2342 }
2443
25- using var httpClient = new HttpClient ( ) ;
26- httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , apiKey ) ;
44+ using var httpClient = CreateOpenAiClient ( apiKey ) ;
2745
28- // Test: simple text input
2946 var requestJson = JsonSerializer . Serialize ( new
3047 {
3148 model = "gpt-4o-mini" ,
3249 input = "Tell me a joke about programming." ,
3350 } ) ;
3451
35- var response = await httpClient . PostAsync (
36- "https://api.openai.com/v1/responses/input_tokens" ,
37- new StringContent ( requestJson , Encoding . UTF8 , "application/json" ) ) ;
52+ var serverTokens = await GetServerTokenCount ( httpClient , requestJson ) ;
3853
39- response . EnsureSuccessStatusCode ( ) ;
40- var responseBody = await response . Content . ReadAsStringAsync ( ) ;
41- using var doc = JsonDocument . Parse ( responseBody ) ;
42- var serverTokens = doc . RootElement . GetProperty ( "input_tokens" ) . GetInt32 ( ) ;
43-
44- // Local count for the same text
4554 var encoder = ModelToEncoder . For ( "gpt-4o-mini" ) ;
4655 var localTokens = encoder . CountTokens ( "Tell me a joke about programming." ) ;
4756
4857 // The server count includes system prompt overhead, so server >= local
49- // We just verify our local count is reasonable (within the server count)
5058 localTokens . Should ( ) . BeGreaterThan ( 0 ) ;
5159 serverTokens . Should ( ) . BeGreaterThanOrEqualTo ( localTokens ,
5260 $ "Server returned { serverTokens } tokens, local counted { localTokens } ") ;
@@ -62,8 +70,7 @@ public async Task ValidateMessageTokensWithToolsAgainstOpenAiApi()
6270 return ;
6371 }
6472
65- using var httpClient = new HttpClient ( ) ;
66- httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Bearer" , apiKey ) ;
73+ using var httpClient = CreateOpenAiClient ( apiKey ) ;
6774
6875 var requestJson = """
6976 {
@@ -95,16 +102,8 @@ public async Task ValidateMessageTokensWithToolsAgainstOpenAiApi()
95102 }
96103 """ ;
97104
98- var response = await httpClient . PostAsync (
99- "https://api.openai.com/v1/responses/input_tokens" ,
100- new StringContent ( requestJson , Encoding . UTF8 , "application/json" ) ) ;
105+ var serverTokens = await GetServerTokenCount ( httpClient , requestJson ) ;
101106
102- response . EnsureSuccessStatusCode ( ) ;
103- var responseBody = await response . Content . ReadAsStringAsync ( ) ;
104- using var doc = JsonDocument . Parse ( responseBody ) ;
105- var serverTokens = doc . RootElement . GetProperty ( "input_tokens" ) . GetInt32 ( ) ;
106-
107- // Local count
108107 var encoder = ModelToEncoder . For ( "gpt-4o-mini" ) ;
109108 var messages = new List < ChatMessage >
110109 {
@@ -121,12 +120,95 @@ public async Task ValidateMessageTokensWithToolsAgainstOpenAiApi()
121120
122121 var localTokens = encoder . CountMessageTokens ( messages , tools ) ;
123122
124- // Log both values for debugging
125123 Console . WriteLine ( $ "Server tokens: { serverTokens } , Local tokens: { localTokens } , Diff: { serverTokens - localTokens } ") ;
126124
127- // Allow reasonable tolerance ( ±20%) since the formula is reverse-engineered
125+ // Allow ±20% tolerance since the formula is reverse-engineered
128126 var tolerance = Math . Max ( serverTokens * 0.2 , 5 ) ;
129127 localTokens . Should ( ) . BeCloseTo ( serverTokens , ( uint ) tolerance ,
130128 $ "Local estimate ({ localTokens } ) should be close to server count ({ serverTokens } )") ;
131129 }
130+
131+ [ TestMethod ]
132+ public async Task ValidateMessagesOnlyAgainstOpenAiApi ( )
133+ {
134+ var apiKey = GetOpenAiApiKey ( ) ;
135+ if ( string . IsNullOrEmpty ( apiKey ) )
136+ {
137+ Assert . Inconclusive ( "OPENAI_API_KEY not set — skipping integration test." ) ;
138+ return ;
139+ }
140+
141+ using var httpClient = CreateOpenAiClient ( apiKey ) ;
142+
143+ // Messages-only test (no tools) to validate message counting
144+ var requestJson = """
145+ {
146+ "model": "gpt-4o-mini",
147+ "input": [
148+ {"role": "developer", "content": "You are a helpful assistant."},
149+ {"role": "user", "content": "hello world"}
150+ ]
151+ }
152+ """ ;
153+
154+ var serverTokens = await GetServerTokenCount ( httpClient , requestJson ) ;
155+
156+ var encoder = ModelToEncoder . For ( "gpt-4o-mini" ) ;
157+ var messages = new List < ChatMessage >
158+ {
159+ new ( "developer" , "You are a helpful assistant." ) ,
160+ new ( "user" , "hello world" ) ,
161+ } ;
162+
163+ var localTokens = encoder . CountMessageTokens ( messages ) ;
164+
165+ Console . WriteLine ( $ "Messages-only: Server={ serverTokens } , Local={ localTokens } , Diff={ serverTokens - localTokens } ") ;
166+
167+ // Messages-only should be very close
168+ localTokens . Should ( ) . BeCloseTo ( serverTokens , 3 ,
169+ $ "Local ({ localTokens } ) should be very close to server ({ serverTokens } ) for messages-only") ;
170+ }
171+
172+ [ TestMethod ]
173+ public async Task ValidateToolsOnlyAgainstOpenAiApi ( )
174+ {
175+ var apiKey = GetOpenAiApiKey ( ) ;
176+ if ( string . IsNullOrEmpty ( apiKey ) )
177+ {
178+ Assert . Inconclusive ( "OPENAI_API_KEY not set — skipping integration test." ) ;
179+ return ;
180+ }
181+
182+ using var httpClient = CreateOpenAiClient ( apiKey ) ;
183+
184+ // Test no-params tool
185+ var requestJson = """
186+ {
187+ "model": "gpt-4o-mini",
188+ "input": [{"role": "user", "content": "hi"}],
189+ "tools": [
190+ {
191+ "type": "function",
192+ "name": "get_time",
193+ "description": "Get the current time"
194+ }
195+ ]
196+ }
197+ """ ;
198+
199+ var serverTokens = await GetServerTokenCount ( httpClient , requestJson ) ;
200+
201+ var encoder = ModelToEncoder . For ( "gpt-4o-mini" ) ;
202+ var messages = new List < ChatMessage > { new ( "user" , "hi" ) } ;
203+ var tools = new List < ChatFunction > { new ( "get_time" , "Get the current time" ) } ;
204+
205+ var localTokens = encoder . CountMessageTokens ( messages , tools ) ;
206+ var toolTokensOnly = encoder . CountToolTokens ( tools ) ;
207+
208+ Console . WriteLine ( $ "No-params tool: Server={ serverTokens } , Local={ localTokens } , ToolTokens={ toolTokensOnly } , Diff={ serverTokens - localTokens } ") ;
209+
210+ var tolerance = Math . Max ( serverTokens * 0.2 , 5 ) ;
211+ localTokens . Should ( ) . BeCloseTo ( serverTokens , ( uint ) tolerance ,
212+ $ "Local ({ localTokens } ) should be close to server ({ serverTokens } )") ;
213+ }
132214}
0 commit comments