Compare commits

..
Author SHA1 Message Date
buzzert b6859706db ios: simplify CI signing keychain setup
TestFlight / testflight (push) Failing after 25s
2026-07-11 11:55:04 -07:00
buzzert ea148839d3 Revert "ios: pass match keychain to codesign"
This reverts commit a0e410155d.
2026-07-11 11:32:00 -07:00
buzzert a0e410155d ios: pass match keychain to codesign
TestFlight / testflight (push) Failing after 55s
2026-07-11 11:29:10 -07:00
buzzert b5a5d21767 docs: document iOS release workflow 2026-07-11 11:17:05 -07:00
buzzert 0ad96f4f99 ios: center transcript content on iPad
TestFlight / testflight (push) Failing after 52s
2026-07-11 11:15:01 -07:00
buzzert b0c2b74c16 remove temporary keychain (unneeded)
TestFlight / testflight (push) Successful in 1m46s
2026-07-06 09:25:24 -07:00
buzzert 14d2d8cb28 fix spacing between lists and paragraphs 2026-07-05 19:54:34 -07:00
buzzert 03836bbc4c Raise Anthropic default max tokens 2026-07-05 11:53:17 -07:00
buzzert 76ce62025a bullets in ul 2026-07-04 20:22:13 -07:00
buzzert b15473d24e ios: stamp release version into XcodeGen spec
TestFlight / testflight (push) Successful in 1m43s
2026-06-26 01:29:54 -07:00
12 changed files with 246 additions and 71 deletions
+6 -19
View File
@@ -8,7 +8,7 @@ on:
jobs: jobs:
testflight: testflight:
runs-on: xcode runs-on: macos-arm64
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -33,29 +33,17 @@ jobs:
brew install xcodegen brew install xcodegen
fi fi
- name: Prepare Runner Keychain - name: Runner diagnostics
env:
HOME: /var/lib/act_runner
run: | run: |
set -euo pipefail set -euo pipefail
mkdir -p "${HOME}/Library/Keychains" whoami
printf 'HOME=%s\n' "$HOME"
login_keychain="${HOME}/Library/Keychains/login.keychain" security default-keychain -d user || true
if [ ! -f "${login_keychain}-db" ]; then security list-keychains -d user || true
security create-keychain -p "" "${login_keychain}"
fi
security unlock-keychain -p "" "${login_keychain}" 2>/dev/null || \
security unlock-keychain -p "sybil-ci-keychain-password" "${login_keychain}" 2>/dev/null || true
security default-keychain -d user -s "${login_keychain}"
security list-keychains -d user -s "${login_keychain}-db"
security delete-keychain "${HOME}/Library/Keychains/sybil_ci_keychain" >/dev/null 2>&1 || true
rm -f "${HOME}/Library/Keychains/sybil_ci_keychain" "${HOME}/Library/Keychains/sybil_ci_keychain-db"
- name: Upload to TestFlight - name: Upload to TestFlight
working-directory: ios working-directory: ios
env: env:
HOME: /var/lib/act_runner
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
@@ -66,6 +54,5 @@ jobs:
FASTLANE_SKIP_UPDATE_CHECK: "1" FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120" FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
run: | run: |
export PATH="/Users/runner/hostedtoolcache/Ruby/3.1.7/arm64/bin:${PATH}"
ruby --version ruby --version
bundle exec fastlane ios beta bundle exec fastlane ios beta
+1
View File
@@ -285,6 +285,7 @@ Behavior notes:
- For `chatId` calls, server stores only *new* non-assistant messages from provided history to avoid duplicates. - For `chatId` calls, server stores only *new* non-assistant messages from provided history to avoid duplicates.
- `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint. - `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint.
- `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools. - `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
- `maxTokens` is optional. For `anthropic`, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as `max_tokens`; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted `maxTokens` is not sent as an explicit cap.
- Server persists final assistant output and call metadata (`LlmCall`) in DB. - Server persists final assistant output and call metadata (`LlmCall`) in DB.
- Server updates chat-level model metadata on each call: `lastUsedProvider`/`lastUsedModel`; first successful/failed call also initializes `initiatedProvider`/`initiatedModel` if unset. - Server updates chat-level model metadata on each call: `lastUsedProvider`/`lastUsedModel`; first successful/failed call also initializes `initiatedProvider`/`initiatedModel` if unset.
- Attachments are optional and currently apply to `user` messages. Persisted chat history stores them under `message.metadata.attachments`. - Attachments are optional and currently apply to `user` messages. Persisted chat history stores them under `message.metadata.attachments`.
+1
View File
@@ -64,6 +64,7 @@ Notes:
- For persisted streams, backend stores only new non-assistant input history rows to avoid duplicates. - For persisted streams, backend stores only new non-assistant input history rows to avoid duplicates.
- `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint. - `additionalSystemPrompt`, when present directly or loaded from stored chat settings, is prepended to the provider request as a `system` message and is not inserted into the persisted chat transcript by this endpoint.
- `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools. - `enabledTools` limits Sybil-managed tools for this request. When omitted for a saved chat, the stored chat setting is used; otherwise all available tools are enabled by default. An empty array disables Sybil-managed tools.
- `maxTokens` is optional. For `anthropic`, when omitted the backend requests the selected model's maximum output token limit from Anthropic's Models API and uses that as `max_tokens`; if the model limit cannot be loaded, the fallback is 128000. For other providers, omitted `maxTokens` is not sent as an explicit cap.
- Attachments are optional and are persisted under `message.metadata.attachments` on stored user messages when `persist` is `true`. - Attachments are optional and are persisted under `message.metadata.attachments` on stored user messages when `persist` is `true`.
Persisted chat streams with a `chatId` are backend-owned active runs: Persisted chat streams with a `chatId` are backend-owned active runs:
+6
View File
@@ -21,6 +21,12 @@ Instructions for work under `/Users/buzzert/src/sybil-2/ios`.
- To choose a screenshot path, run `just screenshot path=build/name.png`. - To choose a screenshot path, run `just screenshot path=build/name.png`.
- The underlying screenshot command is `xcrun simctl io booted screenshot <path>` and requires a booted simulator. - The underlying screenshot command is `xcrun simctl io booted screenshot <path>` and requires a booted simulator.
## Release Workflow
- iOS release tags use the annotated tag namespace `release/ios/vX.Y.Z`; increment from the latest existing `release/ios/v*` tag.
- Tag message convention is `ios: X.Y.Z`, for example `git tag -a release/ios/v1.13.5 -m "ios: 1.13.5"`.
- Push the release commit and tag together with `git push origin <branch> release/ios/vX.Y.Z`.
- Fastlane derives the marketing version from the release tag and stamps `ios/Apps/Sybil/project.yml` during CI, so do not manually bump `MARKETING_VERSION` for normal tagged releases unless explicitly requested.
## App Structure ## App Structure
- App target entry: `/Users/buzzert/src/sybil-2/ios/Apps/Sybil/Sources/SybilApp.swift` - App target entry: `/Users/buzzert/src/sybil-2/ios/Apps/Sybil/Sources/SybilApp.swift`
- Shared iOS app code lives in Swift package: - Shared iOS app code lives in Swift package:
+1 -1
View File
@@ -24,7 +24,7 @@ targets:
GENERATE_INFOPLIST_FILE: YES GENERATE_INFOPLIST_FILE: YES
INFOPLIST_FILE: Apps/Sybil/Info.plist INFOPLIST_FILE: Apps/Sybil/Info.plist
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
MARKETING_VERSION: "1.10" MARKETING_VERSION: "1.13.2"
CURRENT_PROJECT_VERSION: 11 CURRENT_PROJECT_VERSION: 11
INFOPLIST_KEY_CFBundleDisplayName: Sybil INFOPLIST_KEY_CFBundleDisplayName: Sybil
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO
@@ -58,7 +58,8 @@ struct SybilChatTranscriptView: View {
.frame(height: 18 + bottomContentInset) .frame(height: 18 + bottomContentInset)
.id(bottomAnchorID) .id(bottomAnchorID)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: SybilLayout.webContentMaxWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.top, 18 + topContentInset) .padding(.top, 18 + topContentInset)
} }
@@ -98,7 +98,8 @@ struct SybilSearchResultsView: View {
.foregroundStyle(SybilTheme.danger) .foregroundStyle(SybilTheme.danger)
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: SybilLayout.webContentMaxWidth, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, 14) .padding(.horizontal, 14)
.padding(.top, 20 + topContentInset) .padding(.top, 20 + topContentInset)
.padding(.bottom, 20 + bottomContentInset) .padding(.bottom, 20 + bottomContentInset)
@@ -64,6 +64,10 @@ extension Font {
} }
} }
enum SybilLayout {
static let webContentMaxWidth: CGFloat = 896
}
enum SybilTheme { enum SybilTheme {
static let background = Color(red: 0.02, green: 0.02, blue: 0.05) static let background = Color(red: 0.02, green: 0.02, blue: 0.05)
static let surface = Color(red: 0.05, green: 0.04, blue: 0.10) static let surface = Color(red: 0.05, green: 0.04, blue: 0.10)
+60 -44
View File
@@ -6,12 +6,10 @@ APP_IDENTIFIER = "net.buzzert.sybil2"
SCHEME = "Sybil" SCHEME = "Sybil"
TEAM_ID = "DQQH5H6GBD" TEAM_ID = "DQQH5H6GBD"
PROFILE_NAME = "Sybil AppStore CI" PROFILE_NAME = "Sybil AppStore CI"
CI_KEYCHAIN_NAME = "sybil_ci_keychain"
CI_KEYCHAIN_PASSWORD = "sybil-ci-keychain-password"
CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db")
IOS_ROOT = File.expand_path("..", __dir__) IOS_ROOT = File.expand_path("..", __dir__)
PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj") PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj")
PROJECT_SPEC = File.join(IOS_ROOT, "project.yml") PROJECT_SPEC = File.join(IOS_ROOT, "project.yml")
APP_PROJECT_SPEC = File.join(IOS_ROOT, "Apps/Sybil/project.yml")
def present?(value) def present?(value)
!value.to_s.strip.empty? !value.to_s.strip.empty?
@@ -49,6 +47,17 @@ def build_number
value.to_i value.to_i
end end
def stamp_marketing_version(version)
contents = File.read(APP_PROJECT_SPEC)
updated = contents.sub(/^(\s*MARKETING_VERSION:\s*).*/, "\\1\"#{version}\"")
if updated == contents
UI.user_error!("Could not find MARKETING_VERSION in #{APP_PROJECT_SPEC}")
end
File.write(APP_PROJECT_SPEC, updated)
end
platform :ios do platform :ios do
private_lane :app_store_api_key do private_lane :app_store_api_key do
app_store_connect_api_key( app_store_connect_api_key(
@@ -59,23 +68,20 @@ platform :ios do
) )
end end
# CI has no login keychain, so create a dedicated throwaway one for match to # CI uses a throwaway keychain that is deleted after the lane exits.
# import the distribution cert into. The runner's launchd job sets
# SessionCreate, so add_to_search_list actually makes it visible to xcodebuild.
private_lane :prepare_ci_keychain do private_lane :prepare_ci_keychain do
next unless ci? next nil unless ci?
delete_keychain(name: CI_KEYCHAIN_NAME) if File.file?(CI_KEYCHAIN_DB_PATH) run_token = "#{Time.now.to_i}_#{Process.pid}"
create_keychain( keychain_name = "fastlane_ci_#{run_token}"
name: CI_KEYCHAIN_NAME,
password: CI_KEYCHAIN_PASSWORD, setup_ci(
unlock: true, force: true,
timeout: 3600, keychain_name: keychain_name,
add_to_search_list: true timeout: 7_200
) )
ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_NAME keychain_name
ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD
end end
private_lane :sync_signing do |options| private_lane :sync_signing do |options|
@@ -100,40 +106,50 @@ platform :ios do
desc "Build and upload to TestFlight" desc "Build and upload to TestFlight"
lane :beta do lane :beta do
prepare_ci_keychain ci_keychain_name = nil
api_key = app_store_api_key begin
ci_keychain_name = prepare_ci_keychain
sh("xcodegen", "--spec", PROJECT_SPEC) api_key = app_store_api_key
increment_version_number(version_number: release_version, xcodeproj: PROJECT_FILE) version = release_version
increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE) stamp_marketing_version(version)
sh("xcodegen", "--spec", PROJECT_SPEC)
sync_signing(api_key: api_key, readonly: true) increment_version_number(version_number: version, xcodeproj: PROJECT_FILE)
increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE)
build_app( sync_signing(api_key: api_key, readonly: true)
project: PROJECT_FILE,
scheme: SCHEME, sh("security find-identity -v -p codesigning") if ci_keychain_name
export_method: "app-store",
codesigning_identity: "Apple Distribution", build_app(
xcargs: [ project: PROJECT_FILE,
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}", scheme: SCHEME,
"CODE_SIGN_STYLE=Manual", export_method: "app-store",
"CODE_SIGN_IDENTITY=Apple\\ Distribution", codesigning_identity: "Apple Distribution",
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}" xcargs: [
].join(" "), "DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
export_options: { "CODE_SIGN_STYLE=Manual",
signingStyle: "manual", "CODE_SIGN_IDENTITY=Apple\\ Distribution",
teamID: TEAM_ID, "PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
provisioningProfiles: { ].join(" "),
APP_IDENTIFIER => PROFILE_NAME export_options: {
signingStyle: "manual",
teamID: TEAM_ID,
provisioningProfiles: {
APP_IDENTIFIER => PROFILE_NAME
}
} }
} )
)
upload_to_testflight( upload_to_testflight(
api_key: api_key, api_key: api_key,
skip_waiting_for_build_processing: true skip_waiting_for_build_processing: true
) )
ensure
delete_keychain(name: ci_keychain_name) if ci_keychain_name
end
end end
end end
+45 -4
View File
@@ -28,6 +28,45 @@ import type { ChatMessage } from "../types.js";
const INTERNAL_CORRECTION = const INTERNAL_CORRECTION =
"Internal correction: the previous assistant message claimed it would run a tool, but no tool call was made. If the task needs an available tool, call it now. Otherwise provide the final answer directly without saying you will run a tool."; "Internal correction: the previous assistant message claimed it would run a tool, but no tool call was made. If the task needs an available tool, call it now. Otherwise provide the final answer directly without saying you will run a tool.";
const DEFAULT_ANTHROPIC_MAX_TOKENS = 128_000;
const MODEL_MAX_TOKENS_CACHE_MS = 24 * 60 * 60 * 1000;
const modelMaxTokensCache = new Map<string, { maxTokens: number; expiresAt: number }>();
function readMaxTokens(value: unknown) {
return Number.isSafeInteger(value) && (value as number) > 0 ? (value as number) : undefined;
}
function getModelInfoMaxTokens(modelInfo: any) {
return readMaxTokens(modelInfo?.max_tokens) ?? readMaxTokens(modelInfo?.maxTokens);
}
async function getMessagesMaxTokens(params: ToolAwareCompletionParams) {
if (params.maxTokens) return params.maxTokens;
const cached = modelMaxTokensCache.get(params.model);
if (cached && cached.expiresAt > Date.now()) return cached.maxTokens;
try {
const retrieve = params.client?.models?.retrieve;
if (typeof retrieve === "function") {
const modelInfo = await retrieve.call(params.client.models, params.model);
const maxTokens = getModelInfoMaxTokens(modelInfo);
if (maxTokens) {
modelMaxTokensCache.set(params.model, {
maxTokens,
expiresAt: Date.now() + MODEL_MAX_TOKENS_CACHE_MS,
});
return maxTokens;
}
}
} catch {
// Fall back to the documented max for Claude Opus 4.8 and related high-output models.
}
return DEFAULT_ANTHROPIC_MAX_TOKENS;
}
function toTools(tools: any[]) { function toTools(tools: any[]) {
return tools return tools
.map((tool) => { .map((tool) => {
@@ -160,11 +199,12 @@ function mergeUsage(acc: Required<ToolAwareUsage>, usage: any) {
export async function completeWithMessagesApi(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> { export async function completeWithMessagesApi(params: ToolAwareCompletionParams): Promise<ToolAwareCompletionResult> {
const enabledTools = getEnabledChatTools(params); const enabledTools = getEnabledChatTools(params);
const maxTokens = await getMessagesMaxTokens(params);
if (!enabledTools.length) { if (!enabledTools.length) {
const response = await params.client.messages.create({ const response = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation), system: buildTopLevelSystemPrompt(params.messages, params.userLocation),
max_tokens: params.maxTokens ?? 1024, max_tokens: maxTokens,
temperature: params.temperature, temperature: params.temperature,
messages: buildBaseMessages(params), messages: buildBaseMessages(params),
} as any); } as any);
@@ -192,7 +232,7 @@ export async function completeWithMessagesApi(params: ToolAwareCompletionParams)
const response = await params.client.messages.create({ const response = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)), system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)),
max_tokens: params.maxTokens ?? 1024, max_tokens: maxTokens,
temperature: params.temperature, temperature: params.temperature,
messages: conversation, messages: conversation,
tools: toTools(enabledTools), tools: toTools(enabledTools),
@@ -248,6 +288,7 @@ export async function completeWithMessagesApi(params: ToolAwareCompletionParams)
export async function* streamWithMessagesApi(params: ToolAwareCompletionParams): AsyncGenerator<ToolAwareStreamingEvent> { export async function* streamWithMessagesApi(params: ToolAwareCompletionParams): AsyncGenerator<ToolAwareStreamingEvent> {
const enabledTools = getEnabledChatTools(params); const enabledTools = getEnabledChatTools(params);
const maxTokens = await getMessagesMaxTokens(params);
if (!enabledTools.length) { if (!enabledTools.length) {
const rawResponses: unknown[] = []; const rawResponses: unknown[] = [];
const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; const usageAcc: Required<ToolAwareUsage> = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
@@ -259,7 +300,7 @@ export async function* streamWithMessagesApi(params: ToolAwareCompletionParams):
const stream = await params.client.messages.create({ const stream = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation), system: buildTopLevelSystemPrompt(params.messages, params.userLocation),
max_tokens: params.maxTokens ?? 1024, max_tokens: maxTokens,
temperature: params.temperature, temperature: params.temperature,
messages: buildBaseMessages(params), messages: buildBaseMessages(params),
stream: true, stream: true,
@@ -315,7 +356,7 @@ export async function* streamWithMessagesApi(params: ToolAwareCompletionParams):
const stream = await params.client.messages.create({ const stream = await params.client.messages.create({
model: params.model, model: params.model,
system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)), system: buildTopLevelSystemPrompt(params.messages, params.userLocation, buildChatToolSystemPrompt(params)),
max_tokens: params.maxTokens ?? 1024, max_tokens: maxTokens,
temperature: params.temperature, temperature: params.temperature,
messages: conversation, messages: conversation,
tools: toTools(enabledTools), tools: toTools(enabledTools),
+88
View File
@@ -140,6 +140,94 @@ test("plain Chat Completions stream does not send Sybil-managed tools", async ()
assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Hi"); assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Hi");
}); });
test("Messages API defaults max_tokens to the Anthropic model maximum", async () => {
let requestBody: any = null;
let retrievedModel: string | null = null;
const client = {
models: {
retrieve: async (model: string) => {
retrievedModel = model;
return { id: model, max_tokens: 128000 };
},
},
messages: {
create: async (body: any) => {
requestBody = body;
return {
content: [{ type: "text", text: "Done" }],
usage: { input_tokens: 1, output_tokens: 1 },
};
},
},
};
const result = await completeWithMessagesApi({
client: client as any,
model: "claude-max-default-test",
messages: [{ role: "user", content: "Say done" }],
});
assert.equal(retrievedModel, "claude-max-default-test");
assert.equal(requestBody?.max_tokens, 128000);
assert.equal(result.text, "Done");
});
test("Messages API preserves explicit maxTokens", async () => {
let requestBody: any = null;
let didRetrieveModel = false;
const client = {
models: {
retrieve: async () => {
didRetrieveModel = true;
return { max_tokens: 128000 };
},
},
messages: {
create: async (body: any) => {
requestBody = body;
return streamFrom([
{
type: "message_start",
message: {
usage: { input_tokens: 1, output_tokens: 0 },
},
},
{
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
},
{
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "Done" },
},
{ type: "content_block_stop", index: 0 },
{
type: "message_delta",
delta: { stop_reason: "end_turn", stop_sequence: null },
usage: { output_tokens: 1 },
},
{ type: "message_stop" },
]);
},
},
};
const events = await collectEvents(
streamWithMessagesApi({
client: client as any,
model: "claude-explicit-max-test",
messages: [{ role: "user", content: "Say done" }],
maxTokens: 4096,
})
);
assert.equal(didRetrieveModel, false);
assert.equal(requestBody?.max_tokens, 4096);
assert.equal(events.at(-1)?.type === "done" ? events.at(-1)?.result.text : null, "Done");
});
test("fetch_url sends browser-like navigation headers", async () => { test("fetch_url sends browser-like navigation headers", async () => {
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []; const fetchCalls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+30 -1
View File
@@ -286,6 +286,14 @@ textarea {
word-break: break-word; word-break: break-word;
} }
.md-content > :first-child {
margin-top: 0;
}
.md-content > :last-child {
margin-bottom: 0;
}
.md-table-scroll { .md-table-scroll {
max-width: 100%; max-width: 100%;
margin: 0.35rem 0 1rem; margin: 0.35rem 0 1rem;
@@ -384,7 +392,8 @@ textarea {
.md-content ul, .md-content ul,
.md-content ol { .md-content ol {
margin-top: 0.65rem; margin-top: 0.85rem;
margin-bottom: 0.85rem;
margin-left: 0; margin-left: 0;
padding-left: 0; padding-left: 0;
list-style: none; list-style: none;
@@ -396,6 +405,26 @@ textarea {
padding-left: 1.35rem; padding-left: 1.35rem;
} }
.md-content ul > li {
position: relative;
padding-left: 1.1rem;
}
.md-content ul > li::before {
content: "";
position: absolute;
left: 0;
top: 0.76em;
width: 0.36rem;
height: 0.36rem;
border-radius: 9999px;
background: hsl(188 86% 62%);
box-shadow:
0 0 0 2px hsl(188 86% 62% / 0.12),
0 0 10px hsl(188 86% 62% / 0.42);
transform: translateY(-50%);
}
.md-content li + li { .md-content li + li {
margin-top: 0.3rem; margin-top: 0.3rem;
} }