Add Alibaba Cloud Model Studio provider#559
Conversation
Features: - WebView-based usage monitoring for Coding Plan - Supports Lite and Pro plans - 5h, 7d, and 30d usage windows - Orange branding (#FF6A00) Testing: - DOM selectors verified with real account - Requires macOS for compilation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81ab581ccf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case synthetic | ||
| case warp | ||
| case openrouter | ||
| case alibaba |
There was a problem hiding this comment.
Add Alibaba descriptor to bootstrap map
Adding .alibaba to UsageProvider here requires a matching entry in ProviderDescriptorRegistry.descriptorsByID (Sources/CodexBarCore/Providers/ProviderDescriptor.swift), but this commit does not add one. Because bootstrap iterates UsageProvider.allCases and calls preconditionFailure when a descriptor is missing, any code path that touches ProviderDescriptorRegistry.metadata/descriptor(for:) will crash with Missing ProviderDescriptor for alibaba (including normal startup flows).
Useful? React with 👍 / 👎.
Fix: Register AlibabaProviderDescriptor in descriptorsByID dict to match the UsageProvider.alibaba case added in Providers.swift
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5679122b90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| UsageSnapshot( | ||
| primary: sessionUsage, | ||
| secondary: weeklyUsage, | ||
| updatedAt: updatedAt, |
There was a problem hiding this comment.
Include monthly window when building usage snapshot
AlibabaUsageSnapshot captures monthlyUsage, but toUsageSnapshot() only forwards primary and secondary. This drops the 30-day metric before it reaches the shared UsageSnapshot model, so Alibaba users cannot see monthly usage despite the fetcher collecting it.
Useful? React with 👍 / 👎.
| dateFormatter.timeZone = TimeZone(identifier: "UTC") | ||
| dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" |
There was a problem hiding this comment.
Parse Alibaba reset timestamps in console timezone
The reset strings parsed here (yyyy-MM-dd HH:mm:ss Reset) carry no timezone information, but the formatter is forced to UTC. For accounts where the console displays local (non-UTC) times, this shifts resetsAt by the timezone offset and produces incorrect reset countdowns and pacing calculations.
Useful? React with 👍 / 👎.
1. Add note explaining monthlyUsage limitation: - UsageSnapshot only supports primary + secondary windows - monthlyUsage (30d) is collected but not forwarded - Document this in toUsageSnapshot() comment 2. Fix timezone parsing for reset timestamps: - Use Asia/Singapore timezone (ap-southeast-1 region) - Console displays local times, not UTC - This ensures correct reset countdown calculations
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf63255000
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| guard let result = try await webView.evaluateJavaScript(script) as? [String: Any?], | ||
| let jsonData = try? JSONSerialization.data(withJSONObject: result), | ||
| let response = try? JSONDecoder().decode(AlibabaConsoleResponse.self, from: jsonData) |
There was a problem hiding this comment.
Fail when DOM selectors return only null usage fields
This decode step treats an object full of null values as a successful parse because every field in AlibabaConsoleResponse is optional, and parseUsageResponse then defaults those missing values to 0%/synthetic reset dates. When selectors drift or the page hasn’t rendered yet, users will see a seemingly valid low-usage snapshot instead of an error, which can hide scraping breakage and mislead quota decisions. Validate that at least one required usage field is present before returning a snapshot.
Useful? React with 👍 / 👎.
| dateFormatter.locale = Locale(identifier: "en_US_POSIX") | ||
| // Use Singapore timezone for ap-southeast-1 region | ||
| // Users in other regions will see their local console times | ||
| dateFormatter.timeZone = TimeZone(identifier: "Asia/Singapore") |
There was a problem hiding this comment.
Parse reset timestamps in the displayed timezone
The reset text has no timezone offset, but this parser always forces Asia/Singapore, so any account/browser showing times in another timezone gets shifted reset dates and incorrect countdowns by multiple hours. The comments here already note users may see local console times, so hardcoding this zone will produce wrong results in those environments; parse in the displayed/local timezone (or derive it explicitly) instead.
Useful? React with 👍 / 👎.
P1 (Critical): Add validation for required fields - Validate at least one usage field is non-null before returning - Prevents returning 0% snapshot when selectors drift - Throws parseFailed error with descriptive message P2 (Timezone): Remove hardcoded Asia/Singapore timezone - Parse reset timestamps using system timezone - Console displays times in user's local region - Hardcoding SGT broke countdowns for users in other regions - Now respects whatever timezone the user's console shows
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7bc59aa4e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let usage5hPercent = parsePercentage(response.usage5h) ?? 0.0 | ||
| let usage7dPercent = parsePercentage(response.usage7d) ?? 0.0 | ||
| let usage30dPercent = parsePercentage(response.usage30d) ?? 0.0 |
There was a problem hiding this comment.
Treat unparsable usage fields as fetch errors
This parser silently converts any missing or malformed percentage string into 0.0, so if one selector drifts or the page renders partial data, the app reports a real-looking 0% value instead of failing the fetch. In that scenario users are shown artificially low usage for that window and can unknowingly exceed quota; required usage fields should be validated as parseable before building a snapshot.
Useful? React with 👍 / 👎.
| let components = string.components(separatedBy: " Reset") | ||
| if let dateString = components.first { |
There was a problem hiding this comment.
Parse reset dates without assuming English "Reset" suffix
Reset parsing currently depends on splitting the text with the literal " Reset"; when the console language changes or the suffix wording differs, parsing falls back to Date() + window, which makes the displayed reset countdown inaccurate on every refresh. This is user-visible for non-English or changed UI text and should be handled by parsing the timestamp independently of localized trailing labels.
Useful? React with 👍 / 👎.
1. parsePercentage now validates input:
- Returns nil for empty strings (not silently 0.0)
- Returns nil if string is not a valid number
- Prevents showing fake 0% when selectors drift
2. parseResetTime no longer assumes English 'Reset' suffix:
- Extract first 19 chars (timestamp: yyyy-MM-dd HH:mm:ss)
- Ignore any trailing localized text ('Reset', 'Reinicia', etc.)
- Works regardless of console language setting
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0092373e00
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| guard response.usage5h != nil || response.usage7d != nil || response.usage30d != nil else { | ||
| throw AlibabaUsageError.parseFailed("No usage fields found - selectors may have drifted or page not rendered") |
There was a problem hiding this comment.
Fail when any expected usage window cannot be parsed
This validation only requires one of the three usage fields to exist, so partial selector drift still produces a “successful” snapshot and parseUsageResponse fills missing/unparseable windows with 0.0. In practice, if the 5h or 7d selector breaks while 30d still resolves, users can be shown near-zero usage for the broken windows instead of getting a parse error, which is a misleading quota signal.
Useful? React with 👍 / 👎.
| UsageSnapshot( | ||
| primary: sessionUsage, | ||
| secondary: weeklyUsage, | ||
| updatedAt: updatedAt, |
There was a problem hiding this comment.
Forward the collected 30-day usage window
The fetcher and snapshot model collect monthlyUsage, but this conversion drops it and only emits primary/secondary windows, so downstream UI/state never receives the 30-day data. As written, the provider advertises 30d support in code comments/features but discards that window before returning the shared snapshot.
Useful? React with 👍 / 👎.
P1 (Critical): Require ALL three usage fields to be present - Changed validation from OR (any 1 of 3) to AND (all 3 required) - If any window selector breaks, fail fast with descriptive error - Lists exactly which fields are missing (5h, 7d, 30d) - Prevents misleading 0.0% quota signals from partial selector drift P2: Forward monthlyUsage as tertiary window - UsageSnapshot supports primary, secondary, AND tertiary RateWindow fields - Changed AlibabaUsageSnapshot to use RateWindow (standard type) instead of custom UsageWindow - toUsageSnapshot() now forwards all three windows: - primary: sessionUsage (5h) - secondary: weeklyUsage (7d) - tertiary: monthlyUsage (30d) ← NOW FORWARDED - Downstream UI will now receive complete 5h/7d/30d quota data
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ee025245d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let usage5hPercent = parsePercentage(response.usage5h) ?? 0.0 | ||
| let usage7dPercent = parsePercentage(response.usage7d) ?? 0.0 | ||
| let usage30dPercent = parsePercentage(response.usage30d) ?? 0.0 |
There was a problem hiding this comment.
Fail on non-numeric usage values instead of defaulting to 0%
This path silently converts parse failures into 0.0 usage for all windows, so if Alibaba changes a selector and the extracted text becomes non-numeric (for example, a label string instead of "46%"), the fetch still succeeds and reports a misleading “empty usage” state rather than surfacing a scrape failure. Because the guard above only checks for nil fields, malformed-but-present fields are treated as valid and can hide real quota exhaustion.
Useful? React with 👍 / 👎.
CRITICAL FIX:
- parseUsageResponse now throws parseFailed error if any percentage parsing fails
- Changed from '?? 0.0' fallback to 'guard let ... else { throw }' pattern
- Each error message includes the actual value that failed to parse
- This ensures selector drift or format changes surface as errors, not fake 0% usage
Before: '46%' → 46.0, 'abc' → 0.0 (silent failure)
After: '46%' → 46.0, 'abc' → throws error with message
This prevents misleading 'empty quota' signals when page format changes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e1771a7af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let consoleURL = "https://modelstudio.console.alibabacloud.com/ap-southeast-1/?tab=globalset#/efm/coding_plan" | ||
|
|
||
| // Navigate to console with cookies | ||
| try await webView.load(url: consoleURL, cookies: cookies, timeout: 30) |
There was a problem hiding this comment.
Honor configured web timeout for Alibaba page load
fetchUsage hardcodes timeout: 30 when loading the dashboard, so this provider ignores ProviderFetchContext.webTimeout used by other web strategies. In slow networks or first-load SPA hydration cases where users raise the global web timeout, Alibaba fetches can still fail early and report parse/missing-field errors despite the configured timeout.
Useful? React with 👍 / 👎.
| try await webView.load(url: consoleURL, cookies: cookies, timeout: 30) | ||
|
|
||
| // Wait for page to fully load | ||
| try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds |
There was a problem hiding this comment.
Wait for usage selectors instead of sleeping a fixed 3s
The scraper waits a fixed 3 seconds and then reads all required selectors once, but the usage cards are populated asynchronously after navigation. When rendering or API calls take longer than 3 seconds, the required fields are still nil and the fetch fails with Missing usage fields, making usage collection flaky under normal slow-network conditions.
Useful? React with 👍 / 👎.
1. Honor configured web timeout: - Changed fetchUsage signature to accept webTimeout parameter - Pass context.webTimeout instead of hardcoding timeout: 30 - Respects user's global web timeout configuration - Prevents early failures on slow networks when user raised timeout 2. Wait for usage selectors instead of fixed 3s sleep: - Removed fixed 3 second Task.sleep - Added polling loop: check for [ref=e168], [ref=e187], [ref=e206] - Poll up to 20 attempts × 500ms = 10 seconds max - Breaks early when all 3 selectors are found - Throws pageLoadFailed if selectors don't appear in 10s - Handles slow networks and async SPA hydration properly Before: Fixed 3s wait → flaky failures on slow renders After: Smart polling up to 10s → robust across network conditions
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: da038a1826
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let script = "document.querySelector('[ref=\"\(selector)\"]')" | ||
| if let element = try await webView.evaluateJavaScript(script) as? [String: Any], | ||
| element["tagName"] != nil { |
There was a problem hiding this comment.
Return a primitive when polling selector readiness
The readiness loop evaluates document.querySelector(...) directly and then casts the result to a dictionary, but that expression returns a DOM Element object. In the WKWebView-backed path this result type is not bridgeable (only JSON-like primitives/collections are), so evaluateJavaScript can throw before parsing ever runs, causing Alibaba fetches to fail consistently in production. Use a primitive expression such as document.querySelector(...) !== null (or read a primitive property) for the polling check.
Useful? React with 👍 / 👎.
The readiness loop was evaluating document.querySelector(...) directly and casting to [String: Any], but WKWebView cannot bridge DOM Element objects to Swift dictionaries - only JSON-like primitives work. Changed to: - Evaluate 'element !== null' which returns a primitive Bool - Check 'if let exists = ... as? Bool, exists' - No DOM object bridging, no evaluateJavaScript exceptions This fixes consistent production failures in the Alibaba fetcher where the polling loop threw before parsing could run.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6cf30ece9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| dashboardURL: "https://modelstudio.console.alibabacloud.com/ap-southeast-1/?tab=globalset#/efm/coding_plan", | ||
| statusPageURL: nil), | ||
| branding: ProviderBranding( | ||
| iconStyle: .custom, |
There was a problem hiding this comment.
Use a declared IconStyle for Alibaba branding
ProviderBranding.iconStyle is typed as IconStyle, but IconStyle in Sources/CodexBarCore/Providers/Providers.swift has no .custom case. Leaving .custom here makes the new provider descriptor fail to type-check, which blocks compilation for any target that includes CodexBarCore.
Useful? React with 👍 / 👎.
|
|
||
| func isAvailable(_ context: ProviderFetchContext) async -> Bool { | ||
| // Check if user has cookies for alibabacloud.com | ||
| return context.cookieImporter.hasCookies(for: "alibabacloud.com") |
There was a problem hiding this comment.
Make AlibabaWebFetchStrategy conform to fetch interfaces
The strategy wiring here does not match the current fetch pipeline contracts: ProviderFetchContext does not expose cookieImporter/webViewAPI, and this block also relies on unsupported strategy typing in the same struct. As a result, the Alibaba provider cannot compile or run through ProviderFetchPlan in this commit.
Useful? React with 👍 / 👎.
…porter ProviderFetchContext does not expose cookieImporter or webViewAPI. Following the CodexWebDashboardStrategy pattern instead: - Use context.browserDetection for cookie access - Use MainActor for WebView operations (AppKit init required) - Check cookies via browserDetection.hasCookies(for:) - Create WebView via browserDetection.createWebView() - Add proper fallback logic for auto mode This matches how OpenAI/Claude web scraping works in CodexBar.
|
Closing as duplicate of #453 |
Features:
Testing: